diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml index cb5b40a5881..97e12da3dbf 100644 --- a/.github/workflows/smoke-checks.yml +++ b/.github/workflows/smoke-checks.yml @@ -68,7 +68,8 @@ jobs: build-old-xcode: name: Build LLC + UI (Xcode 15) runs-on: macos-15 - if: ${{ github.event.inputs.record_snapshots != 'true' }} + # if: ${{ github.event.inputs.record_snapshots != 'true' }} + if: false # disable Xcode 15 builds env: XCODE_VERSION: "15.4" steps: diff --git a/.swiftformat b/.swiftformat index 7cd15b4c1cf..808ece6758c 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,6 +1,6 @@ # Stream rules --header "\nCopyright © {year} Stream.io Inc. All rights reserved.\n" ---swiftversion 5.6 +--swiftversion 6.0 --ifdef no-indent --disable redundantType diff --git a/DemoApp/Screens/Create Chat/CreateChatViewController.swift b/DemoApp/Screens/Create Chat/CreateChatViewController.swift index 023cddd1dae..7c7ef6793a5 100644 --- a/DemoApp/Screens/Create Chat/CreateChatViewController.swift +++ b/DemoApp/Screens/Create Chat/CreateChatViewController.swift @@ -25,19 +25,21 @@ class CreateChatViewController: UIViewController { // Create the Channel on backend controller.synchronize { [weak self] error in - if let error = error { - self?.presentAlert(title: "Error when creating the channel", message: error.localizedDescription) - return + Task { @MainActor in + if let error = error { + self?.presentAlert(title: "Error when creating the channel", message: error.localizedDescription) + return + } + + // Send the message + createMessage(text) + + // Present the new chat and controller + let vc = ChatChannelVC() + vc.channelController = controller + + navController.setViewControllers([navController.viewControllers.first!, vc], animated: true) } - - // Send the message - createMessage(text) - - // Present the new chat and controller - let vc = ChatChannelVC() - vc.channelController = controller - - navController.setViewControllers([navController.viewControllers.first!, vc], animated: true) } } } @@ -143,9 +145,11 @@ class CreateChatViewController: UIViewController { ]) // Empty initial search to get all users - searchController.search(term: nil) { error in + searchController.search(term: nil) { [weak self] error in if error != nil { - self.update(for: .error) + Task { @MainActor in + self?.update(for: .error) + } } } infoLabel.text = "On the platform" @@ -235,9 +239,11 @@ class CreateChatViewController: UIViewController { operation?.cancel() operation = DispatchWorkItem { [weak self] in - self?.searchController.search(term: sender.text) { error in + self?.searchController.search(term: sender.text) { [weak self] error in if error != nil { - self?.update(for: .error) + Task { @MainActor in + self?.update(for: .error) + } } } } diff --git a/DemoApp/Screens/Create Chat/NameGroupViewController.swift b/DemoApp/Screens/Create Chat/NameGroupViewController.swift index 84f5ac61b7e..798807367a8 100644 --- a/DemoApp/Screens/Create Chat/NameGroupViewController.swift +++ b/DemoApp/Screens/Create Chat/NameGroupViewController.swift @@ -131,11 +131,13 @@ class NameGroupViewController: UIViewController { name: name, members: Set(selectedUsers.map(\.id)) ) - channelController?.synchronize { error in - if let error = error { - self.presentAlert(title: "Error when creating the channel", message: error.localizedDescription) - } else { - self.navigationController?.popToRootViewController(animated: true) + channelController?.synchronize { [weak self] error in + Task { @MainActor in + if let error = error { + self?.presentAlert(title: "Error when creating the channel", message: error.localizedDescription) + } else { + self?.navigationController?.popToRootViewController(animated: true) + } } } } catch { diff --git a/DemoApp/Screens/DemoDraftMessageListVC.swift b/DemoApp/Screens/DemoDraftMessageListVC.swift index 2765be5d8e5..6805dd09c97 100644 --- a/DemoApp/Screens/DemoDraftMessageListVC.swift +++ b/DemoApp/Screens/DemoDraftMessageListVC.swift @@ -94,7 +94,9 @@ class DemoDraftMessageListVC: UIViewController, ThemeProvider { currentUserController.delegate = self loadingIndicator.startAnimating() currentUserController.loadDraftMessages { [weak self] _ in - self?.loadingIndicator.stopAnimating() + Task { @MainActor in + self?.loadingIndicator.stopAnimating() + } } } @@ -105,7 +107,9 @@ class DemoDraftMessageListVC: UIViewController, ThemeProvider { isPaginatingDrafts = true currentUserController.loadMoreDraftMessages { [weak self] _ in - self?.isPaginatingDrafts = false + Task { @MainActor in + self?.isPaginatingDrafts = false + } } } diff --git a/DemoApp/Screens/MembersViewController.swift b/DemoApp/Screens/MembersViewController.swift index 83c137fabe4..04161f0c44d 100644 --- a/DemoApp/Screens/MembersViewController.swift +++ b/DemoApp/Screens/MembersViewController.swift @@ -37,7 +37,9 @@ class MembersViewController: UITableViewController, ChatChannelMemberListControl private func synchronizeAndUpdateData() { membersController.synchronize { [weak self] _ in - self?.updateData() + Task { @MainActor in + self?.updateData() + } } } diff --git a/DemoApp/Shared/StreamChatWrapper.swift b/DemoApp/Shared/StreamChatWrapper.swift index c113aa538e8..23e6e768fd6 100644 --- a/DemoApp/Shared/StreamChatWrapper.swift +++ b/DemoApp/Shared/StreamChatWrapper.swift @@ -8,7 +8,7 @@ import StreamChatUI import UIKit import UserNotifications -final class StreamChatWrapper { +final class StreamChatWrapper: @unchecked Sendable { static var shared = StreamChatWrapper(apiKeyString: apiKeyString) static func replaceSharedInstance(apiKeyString: String) { @@ -56,7 +56,7 @@ extension StreamChatWrapper { // MARK: User Authentication extension StreamChatWrapper { - func connect(user: DemoUserType, completion: @escaping (Error?) -> Void) { + func connect(user: DemoUserType, completion: @escaping @Sendable(Error?) -> Void) { switch user { case let .credentials(userCredentials): connectUser(credentials: userCredentials, completion: completion) @@ -69,7 +69,7 @@ extension StreamChatWrapper { } } - func connectUser(credentials: UserCredentials?, completion: @escaping (Error?) -> Void) { + func connectUser(credentials: UserCredentials?, completion: @escaping @Sendable(Error?) -> Void) { guard let userCredentials = credentials else { log.error("User credentials are missing") return @@ -187,7 +187,7 @@ extension StreamChatWrapper { // An object to test the Stream Models transformer. // By default it is not used. To use it, set it to the `modelsTransformer` property of the `ChatClientConfig`. -class CustomStreamModelsTransformer: StreamModelsTransformer { +final class CustomStreamModelsTransformer: StreamModelsTransformer { func transform(channel: ChatChannel) -> ChatChannel { channel.replacing( name: "Custom Name", diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift index 766207b8674..95bd96aa05c 100644 --- a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift +++ b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift @@ -128,10 +128,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { team: channelController.channel?.team ) { [unowned self] error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't update name of channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't update name of channel \(cid)", + message: "\(error)" + ) + } } } } @@ -152,10 +154,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { extraData: channelController.channel?.extraData ?? [:] ) { [unowned self] error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't update image url of channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't update image url of channel \(cid)", + message: "\(error)" + ) + } } } } @@ -203,23 +207,27 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { let client = channelController.client client.currentUserController().loadBlockedUsers { result in guard let blockedUsers = try? result.get() else { return } - self.rootViewController.present(MembersViewController( - membersController: client.memberListController( - query: .init( - cid: cid, - filter: .in(.id, values: blockedUsers.map(\.userId)) + Task { @MainActor in + self.rootViewController.present(MembersViewController( + membersController: client.memberListController( + query: .init( + cid: cid, + filter: .in(.id, values: blockedUsers.map(\.userId)) + ) ) - ) - ), animated: true) + ), animated: true) + } } }), .init(title: "Load More Members", handler: { [unowned self] _ in channelController.loadMoreChannelReads(limit: 100) { error in guard let error else { return } - self.rootViewController.presentAlert( - title: "Couldn't load more members to channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't load more members to channel \(cid)", + message: "\(error)" + ) + } } }), .init(title: "Add member", isEnabled: canUpdateChannelMembers, handler: { [unowned self] _ in @@ -233,10 +241,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { message: "Members added to the channel" ) { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't add user \(id) to channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't add user \(id) to channel \(cid)", + message: "\(error)" + ) + } } } } @@ -253,10 +263,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { message: "Members added to the channel" ) { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't add user \(id) to channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't add user \(id) to channel \(cid)", + message: "\(error)" + ) + } } } } @@ -268,13 +280,15 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { userIds: [member.id], message: "Members removed from the channel" ) { [unowned self] error in - if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't remove user \(member.id) from channel \(cid)", - message: "\(error)" - ) - } else { - self.rootNavigationController?.popViewController(animated: true) + Task { @MainActor in + if let error = error { + self.rootViewController.presentAlert( + title: "Couldn't remove user \(member.id) from channel \(cid)", + message: "\(error)" + ) + } else { + self.rootNavigationController?.popViewController(animated: true) + } } } } @@ -288,10 +302,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .memberController(userId: member.id, in: channelController.cid!) .ban { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't ban user \(member.id) from channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't ban user \(member.id) from channel \(cid)", + message: "\(error)" + ) + } } } } @@ -305,10 +321,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .memberController(userId: member.id, in: channelController.cid!) .shadowBan { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't ban user \(member.id) from channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't ban user \(member.id) from channel \(cid)", + message: "\(error)" + ) + } } } } @@ -322,10 +340,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .memberController(userId: member.id, in: channelController.cid!) .unban { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't unban user \(member.id) from channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't unban user \(member.id) from channel \(cid)", + message: "\(error)" + ) + } } } } @@ -343,10 +363,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { message: "Premium member added to the channel" ) { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't add user \(id) to channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't add user \(id) to channel \(cid)", + message: "\(error)" + ) + } } } } @@ -356,15 +378,17 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { UIAlertAction(title: member.id, style: .default) { _ in channelController.client.memberController(userId: member.id, in: cid) .partialUpdate(extraData: ["is_premium": true], unsetProperties: nil) { [unowned self] result in - do { - let data = try result.get() - print("Member updated. Premium: ", data.isPremium) - self.rootNavigationController?.popViewController(animated: true) - } catch { - self.rootViewController.presentAlert( - title: "Couldn't set user \(member.id) as premium.", - message: "\(error)" - ) + Task { @MainActor in + do { + let data = try result.get() + print("Member updated. Premium: ", data.isPremium) + self.rootNavigationController?.popViewController(animated: true) + } catch { + self.rootViewController.presentAlert( + title: "Couldn't set user \(member.id) as premium.", + message: "\(error)" + ) + } } } } @@ -374,15 +398,17 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .init(title: "Set current member as premium", isVisible: isPremiumMemberFeatureEnabled, handler: { [unowned self] _ in channelController.client.currentUserController() .updateMemberData(["is_premium": true], in: cid) { [unowned self] result in - do { - let data = try result.get() - print("Member updated. Premium: ", data.isPremium) - self.rootNavigationController?.popViewController(animated: true) - } catch { - self.rootViewController.presentAlert( - title: "Couldn't set current user as premium.", - message: "\(error)" - ) + Task { @MainActor in + do { + let data = try result.get() + print("Member updated. Premium: ", data.isPremium) + self.rootNavigationController?.popViewController(animated: true) + } catch { + self.rootViewController.presentAlert( + title: "Couldn't set current user as premium.", + message: "\(error)" + ) + } } } }), @@ -391,15 +417,17 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { UIAlertAction(title: member.id, style: .default) { _ in channelController.client.memberController(userId: member.id, in: cid) .partialUpdate(extraData: nil, unsetProperties: ["is_premium"]) { [unowned self] result in - do { - let data = try result.get() - print("Member updated. Premium: ", data.isPremium) - self.rootNavigationController?.popViewController(animated: true) - } catch { - self.rootViewController.presentAlert( - title: "Couldn't set user \(member.id) as premium.", - message: "\(error)" - ) + Task { @MainActor in + do { + let data = try result.get() + print("Member updated. Premium: ", data.isPremium) + self.rootNavigationController?.popViewController(animated: true) + } catch { + self.rootViewController.presentAlert( + title: "Couldn't set user \(member.id) as premium.", + message: "\(error)" + ) + } } } } @@ -409,21 +437,27 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .init(title: "Freeze channel", isEnabled: canFreezeChannel, handler: { [unowned self] _ in channelController.freezeChannel { error in if let error = error { - self.rootViewController.presentAlert(title: "Couldn't freeze channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't freeze channel \(cid)", message: "\(error)") + } } } }), .init(title: "Unfreeze channel", isEnabled: canFreezeChannel, handler: { [unowned self] _ in channelController.unfreezeChannel { error in if let error = error { - self.rootViewController.presentAlert(title: "Couldn't unfreeze channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't unfreeze channel \(cid)", message: "\(error)") + } } } }), .init(title: "Mute channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in channelController.muteChannel { error in if let error = error { - self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)") + } } } }), @@ -435,7 +469,9 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { } channelController.muteChannel(expiration: expiration * 1000) { error in if let error = error { - self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)") + } } } } @@ -443,46 +479,60 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .init(title: "Cool channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in channelController.partialChannelUpdate(extraData: ["is_cool": true]) { error in if let error = error { - self.rootViewController.presentAlert(title: "Couldn't make a channel \(cid) cool", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't make a channel \(cid) cool", message: "\(error)") + } } } }), .init(title: "Uncool channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in channelController.partialChannelUpdate(extraData: ["is_cool": false]) { error in if let error = error { - self.rootViewController.presentAlert(title: "Couldn't make a channel \(cid) uncool", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't make a channel \(cid) uncool", message: "\(error)") + } } } }), .init(title: "Unmute channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in channelController.unmuteChannel { error in if let error = error { - self.rootViewController.presentAlert(title: "Couldn't unmute channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't unmute channel \(cid)", message: "\(error)") + } } } }), .init(title: "Pin channel", isEnabled: true, handler: { [unowned self] _ in channelController.pin { error in guard let error else { return } - self.rootViewController.presentAlert(title: "Couldn't pin channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't pin channel \(cid)", message: "\(error)") + } } }), .init(title: "Unpin channel", isEnabled: true, handler: { [unowned self] _ in channelController.unpin { error in guard let error else { return } - self.rootViewController.presentAlert(title: "Couldn't unpin channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't unpin channel \(cid)", message: "\(error)") + } } }), .init(title: "Archive channel", isEnabled: true, handler: { [unowned self] _ in channelController.archive { error in guard let error else { return } - self.rootViewController.presentAlert(title: "Couldn't archive channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't archive channel \(cid)", message: "\(error)") + } } }), .init(title: "Unarchive channel", isEnabled: true, handler: { [unowned self] _ in channelController.unarchive { error in guard let error else { return } - self.rootViewController.presentAlert(title: "Couldn't unarchive channel \(cid)", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Couldn't unarchive channel \(cid)", message: "\(error)") + } } }), .init(title: "Enable slow mode", isEnabled: canSetChannelCooldown, handler: { [unowned self] _ in @@ -494,10 +544,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { } channelController.enableSlowMode(cooldownDuration: duration) { [unowned self] error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't enable slow mode on channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't enable slow mode on channel \(cid)", + message: "\(error)" + ) + } } } } @@ -505,10 +557,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .init(title: "Disable slow mode", isEnabled: canSetChannelCooldown, handler: { [unowned self] _ in channelController.disableSlowMode { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't disable slow mode on channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't disable slow mode on channel \(cid)", + message: "\(error)" + ) + } } } }), @@ -520,20 +574,24 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .init(title: "Clear History", handler: { _ in channelController.hideChannel(clearHistory: true) { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't hide channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't hide channel \(cid)", + message: "\(error)" + ) + } } } }), .init(title: "Keep History", handler: { _ in channelController.hideChannel(clearHistory: false) { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't hide channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't hide channel \(cid)", + message: "\(error)" + ) + } } } }) @@ -544,30 +602,36 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { .init(title: "Show channel", isEnabled: channelController.channel?.isHidden == true, handler: { [unowned self] _ in channelController.showChannel { error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't unhide channel \(cid)", - message: "\(error)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't unhide channel \(cid)", + message: "\(error)" + ) + } } } }), .init(title: "Truncate channel w/o message", isEnabled: canUpdateChannel, handler: { _ in channelController.truncateChannel { [unowned self] error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't truncate channel \(cid)", - message: "\(error.localizedDescription)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't truncate channel \(cid)", + message: "\(error.localizedDescription)" + ) + } } } }), .init(title: "Truncate channel with message", isEnabled: canUpdateChannel, handler: { _ in channelController.truncateChannel(systemMessage: "Channel truncated") { [unowned self] error in if let error = error { - self.rootViewController.presentAlert( - title: "Couldn't truncate channel \(cid)", - message: "\(error.localizedDescription)" - ) + Task { @MainActor in + self.rootViewController.presentAlert( + title: "Couldn't truncate channel \(cid)", + message: "\(error.localizedDescription)" + ) + } } } }), @@ -620,19 +684,21 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { let messageController = channelController.client.messageController(cid: cid, messageId: id) messageController.synchronize { [weak self] error in - let message = messageController.message - - var errorMessage: String? = error?.localizedDescription - if message?.cid != cid { - errorMessage = "Message ID does not belong to this channel." - } - - if let errorMessage = errorMessage { - self?.rootViewController.presentAlert(title: errorMessage) - return + Task { @MainActor in + let message = messageController.message + + var errorMessage: String? = error?.localizedDescription + if message?.cid != cid { + errorMessage = "Message ID does not belong to this channel." + } + + if let errorMessage = errorMessage { + self?.rootViewController.presentAlert(title: errorMessage) + return + } + + self?.showChannel(for: cid, at: message?.id) } - - self?.showChannel(for: cid, at: message?.id) } } }), @@ -654,7 +720,9 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { channelController.client.currentUserController() .updateUserData(unsetProperties: ["image"]) { [unowned self] error in if let error { - self.rootViewController.presentAlert(title: error.localizedDescription) + Task { @MainActor in + self.rootViewController.presentAlert(title: error.localizedDescription) + } } } }), @@ -684,7 +752,9 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { override func didTapDeleteButton(for cid: ChannelId) { rootViewController.controller.client.channelController(for: cid).deleteChannel { error in if let error = error { - self.rootViewController.presentAlert(title: "Channel \(cid) couldn't be deleted", message: "\(error)") + Task { @MainActor in + self.rootViewController.presentAlert(title: "Channel \(cid) couldn't be deleted", message: "\(error)") + } } } } diff --git a/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift b/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift index e6daf818060..4f24699fc78 100644 --- a/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift +++ b/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift @@ -37,16 +37,20 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC { guard confirmed else { return } self.messageController.deleteMessage { _ in - let pollId = self.messageController.message?.poll?.id - if let pollId, AppConfig.shared.demoAppConfig.shouldDeletePollOnMessageDeletion { - let channelController = self.messageController.client.channelController( - for: self.messageController.cid - ) - channelController.deletePoll(pollId: pollId) { _ in + Task { @MainActor in + let pollId = self.messageController.message?.poll?.id + if let pollId, AppConfig.shared.demoAppConfig.shouldDeletePollOnMessageDeletion { + let channelController = self.messageController.client.channelController( + for: self.messageController.cid + ) + channelController.deletePoll(pollId: pollId) { _ in + Task { @MainActor in + self.delegate?.chatMessageActionsVCDidFinish(self) + } + } + } else { self.delegate?.chatMessageActionsVCDidFinish(self) } - } else { - self.delegate?.chatMessageActionsVCDidFinish(self) } } } @@ -65,14 +69,18 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC { if let error = error { log.error("Error when pinning message: \(error)") } - self.delegate?.chatMessageActionsVCDidFinish(self) + Task { @MainActor in + self.delegate?.chatMessageActionsVCDidFinish(self) + } } } else { self.messageController.unpin { error in if let error = error { log.error("Error when unpinning message: \(error)") } - self.delegate?.chatMessageActionsVCDidFinish(self) + Task { @MainActor in + self.delegate?.chatMessageActionsVCDidFinish(self) + } } } }, @@ -88,7 +96,9 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC { guard confirmed else { return } self.messageController.deleteMessage(hard: true) { _ in - self.delegate?.chatMessageActionsVCDidFinish(self) + Task { @MainActor in + self.delegate?.chatMessageActionsVCDidFinish(self) + } } } }, @@ -101,7 +111,9 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC { action: { [weak self] _ in guard let self = self else { return } self.messageController.translate(to: .turkish) { _ in - self.delegate?.chatMessageActionsVCDidFinish(self) + Task { @MainActor in + self.delegate?.chatMessageActionsVCDidFinish(self) + } } }, diff --git a/DemoApp/StreamChat/Components/DemoChatThreadVC.swift b/DemoApp/StreamChat/Components/DemoChatThreadVC.swift index 319cf930cc9..0d1e23b0582 100644 --- a/DemoApp/StreamChat/Components/DemoChatThreadVC.swift +++ b/DemoApp/StreamChat/Components/DemoChatThreadVC.swift @@ -54,7 +54,9 @@ class DemoChatThreadVC: ChatThreadVC, CurrentChatUserControllerDelegate { return } self.messageController.updateThread(title: title) { [weak self] result in - self?.thread = try? result.get() + Task { @MainActor in + self?.thread = try? result.get() + } } } }), @@ -63,8 +65,10 @@ class DemoChatThreadVC: ChatThreadVC, CurrentChatUserControllerDelegate { }), .init(title: "Load newest thread info", style: .default, handler: { [unowned self] _ in self.messageController.loadThread { [weak self] result in - self?.thread = try? result.get() - self?.present(DebugObjectViewController(object: self?.thread), animated: true) + Task { @MainActor in + self?.thread = try? result.get() + self?.present(DebugObjectViewController(object: self?.thread), animated: true) + } } }) ]) diff --git a/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift b/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift index e63244d3ff0..3b741bfad7b 100644 --- a/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift +++ b/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift @@ -218,7 +218,7 @@ private extension DemoAppCoordinator { func disconnect() { chat.client?.disconnect { [weak self] in - DispatchQueue.main.async { + Task { @MainActor in self?.showLogin(animated: true) } } diff --git a/DemoShare/DemoShareViewModel.swift b/DemoShare/DemoShareViewModel.swift index 78c14b04ef2..0b4658df68b 100644 --- a/DemoShare/DemoShareViewModel.swift +++ b/DemoShare/DemoShareViewModel.swift @@ -178,8 +178,10 @@ class DemoShareViewModel: ObservableObject, ChatChannelControllerDelegate { ) self.channelListController = chatClient.channelListController(query: channelListQuery) channelListController?.synchronize { [weak self] error in - guard let self, error == nil else { return } - channels = channelListController?.channels ?? [] + Task { @MainActor [weak self] in + guard let self, error == nil else { return } + channels = channelListController?.channels ?? [] + } } } } diff --git a/Package.swift b/Package.swift index 17c6eec1c8a..605f6188188 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:6.0 import Foundation import PackageDescription @@ -51,7 +51,10 @@ let package = Package( dependencies: ["StreamChat"], path: "TestTools/StreamChatTestMockServer", exclude: ["Info.plist"], - resources: [.process("Fixtures")] + resources: [.process("Fixtures")], + swiftSettings: [ + .swiftLanguageMode(.v5) + ] ), ] ) diff --git a/README.md b/README.md index 04f85c8f7e2..035ed83ce85 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

- +

diff --git a/Sources/StreamChat/APIClient/APIClient.swift b/Sources/StreamChat/APIClient/APIClient.swift index c63078301b8..b7806679e52 100644 --- a/Sources/StreamChat/APIClient/APIClient.swift +++ b/Sources/StreamChat/APIClient/APIClient.swift @@ -5,7 +5,7 @@ import Foundation /// An object allowing making request to Stream Chat servers. -class APIClient { +class APIClient: @unchecked Sendable { /// The URL session used for all requests. let session: URLSession @@ -16,10 +16,10 @@ class APIClient { let decoder: RequestDecoder /// Used for reobtaining tokens when they expire and API client receives token expiration error - var tokenRefresher: ((@escaping () -> Void) -> Void)? + @Atomic var tokenRefresher: ((@escaping @Sendable() -> Void) -> Void)? /// Used to queue requests that happen while we are offline - var queueOfflineRequest: QueueOfflineRequestBlock? + @Atomic var queueOfflineRequest: QueueOfflineRequestBlock? /// The attachment downloader. let attachmentDownloader: AttachmentDownloader @@ -79,7 +79,7 @@ class APIClient { /// - completion: Called when the networking request is finished. func request( endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { let requestOperation = operation(endpoint: endpoint, isRecoveryOperation: false, completion: completion) operationQueue.addOperation(requestOperation) @@ -92,7 +92,7 @@ class APIClient { /// - completion: Called when the networking request is finished. func recoveryRequest( endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { if !isInRecoveryMode { log.assertionFailure("We should not call this method if not in recovery mode") @@ -110,7 +110,7 @@ class APIClient { /// - completion: Called when the networking request is finished. func unmanagedRequest( endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { OperationQueue.main.addOperation( unmanagedOperation(endpoint: endpoint, completion: completion) @@ -120,7 +120,7 @@ class APIClient { private func operation( endpoint: Endpoint, isRecoveryOperation: Bool, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) -> AsyncOperation { AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in guard let self = self else { @@ -189,7 +189,7 @@ class APIClient { private func unmanagedOperation( endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) -> AsyncOperation { AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in self?.executeRequest(endpoint: endpoint) { [weak self] result in @@ -219,7 +219,7 @@ class APIClient { /// - completion: Called when the networking request is finished. private func executeRequest( endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { encoder.encodeRequest(for: endpoint) { [weak self] (requestResult) in let urlRequest: URLRequest @@ -268,7 +268,7 @@ class APIClient { } } - private func refreshToken(completion: @escaping (ClientError) -> Void) { + private func refreshToken(completion: @escaping @Sendable(ClientError) -> Void) { guard !isRefreshingToken else { completion(ClientError.RefreshingToken()) return @@ -290,11 +290,11 @@ class APIClient { func downloadFile( from remoteURL: URL, to localURL: URL, - progress: ((Double) -> Void)?, - completion: @escaping (Error?) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Error?) -> Void ) { let downloadOperation = AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in - self?.attachmentDownloader.download(from: remoteURL, to: localURL, progress: progress) { error in + self?.attachmentDownloader.download(from: remoteURL, to: localURL, progress: progress) { [weak self] error in if let error, self?.isConnectionError(error) == true { // Do not retry unless its a connection problem and we still have retries left if operation.canRetry { @@ -314,11 +314,11 @@ class APIClient { func uploadAttachment( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Result) -> Void ) { let uploadOperation = AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in - self?.attachmentUploader.upload(attachment, progress: progress) { result in + self?.attachmentUploader.upload(attachment, progress: progress) { [weak self] result in switch result { case let .failure(error) where self?.isConnectionError(error) == true: // Do not retry unless its a connection problem and we still have retries left diff --git a/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift b/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift index 95f1c604ce8..3f3a363afd2 100644 --- a/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift +++ b/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift @@ -16,14 +16,15 @@ protocol AttachmentDownloader { func download( from remoteURL: URL, to localURL: URL, - progress: ((Double) -> Void)?, - completion: @escaping (Error?) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Error?) -> Void ) } -final class StreamAttachmentDownloader: AttachmentDownloader { +final class StreamAttachmentDownloader: AttachmentDownloader, Sendable { private let session: URLSession - @Atomic private var taskProgressObservers: [Int: NSKeyValueObservation] = [:] + nonisolated(unsafe) private var _taskProgressObservers: [Int: NSKeyValueObservation] = [:] + private let queue = DispatchQueue(label: "io.getstream.stream-attachment-downloader", target: .global()) init(sessionConfiguration: URLSessionConfiguration) { session = URLSession(configuration: sessionConfiguration) @@ -32,8 +33,8 @@ final class StreamAttachmentDownloader: AttachmentDownloader { func download( from remoteURL: URL, to localURL: URL, - progress: ((Double) -> Void)?, - completion: @escaping (Error?) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Error?) -> Void ) { let request = URLRequest(url: remoteURL) let task = session.downloadTask(with: request) { temporaryURL, _, downloadError in @@ -54,13 +55,13 @@ final class StreamAttachmentDownloader: AttachmentDownloader { } if let progressHandler = progress { let taskID = task.taskIdentifier - _taskProgressObservers.mutate { observers in - observers[taskID] = task.progress.observe(\.fractionCompleted, options: [.initial]) { [weak self] progress, _ in + queue.async { [weak self] in + self?._taskProgressObservers[taskID] = task.progress.observe(\.fractionCompleted, options: [.initial]) { [weak self] progress, _ in progressHandler(progress.fractionCompleted) if progress.isFinished || progress.isCancelled { - self?._taskProgressObservers.mutate { observers in - observers[taskID]?.invalidate() - observers[taskID] = nil + self?.queue.async { [weak self] in + self?._taskProgressObservers[taskID]?.invalidate() + self?._taskProgressObservers[taskID] = nil } } } diff --git a/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift b/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift index 33ae7ef0832..073b85120d1 100644 --- a/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift +++ b/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift @@ -5,7 +5,7 @@ import Foundation /// The component responsible to upload files. -public protocol AttachmentUploader { +public protocol AttachmentUploader: Sendable { /// Uploads a type-erased attachment, and returns the attachment with the remote information. /// - Parameters: /// - attachment: A type-erased attachment. @@ -13,12 +13,12 @@ public protocol AttachmentUploader { /// - completion: The callback with the uploaded attachment. func upload( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Result) -> Void ) } -public class StreamAttachmentUploader: AttachmentUploader { +public class StreamAttachmentUploader: AttachmentUploader, @unchecked Sendable { let cdnClient: CDNClient init(cdnClient: CDNClient) { @@ -27,8 +27,8 @@ public class StreamAttachmentUploader: AttachmentUploader { public func upload( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Result) -> Void ) { cdnClient.uploadAttachment(attachment, progress: progress) { (result: Result) in completion(result.map { file in diff --git a/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachment.swift b/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachment.swift index 2dee646b0e3..3ed99a53927 100644 --- a/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachment.swift +++ b/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachment.swift @@ -5,7 +5,7 @@ import Foundation /// The attachment which was successfully uploaded. -public struct UploadedAttachment { +public struct UploadedAttachment: Sendable { /// The attachment which contains the payload details of the attachment. public var attachment: AnyChatMessageAttachment diff --git a/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachmentPostProcessor.swift b/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachmentPostProcessor.swift index 047cbcf8102..10f53e21f89 100644 --- a/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachmentPostProcessor.swift +++ b/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachmentPostProcessor.swift @@ -5,6 +5,6 @@ import Foundation /// A component that can be used to change an attachment which was successfully uploaded. -public protocol UploadedAttachmentPostProcessor { +public protocol UploadedAttachmentPostProcessor: Sendable { func process(uploadedAttachment: UploadedAttachment) -> UploadedAttachment } diff --git a/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift b/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift index facef3c0f28..bd0ee74ab7b 100644 --- a/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift +++ b/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift @@ -16,7 +16,7 @@ public struct UploadedFile: Decodable { } /// The CDN client is responsible to upload files to a CDN. -public protocol CDNClient { +public protocol CDNClient: Sendable { static var maxAttachmentSize: Int64 { get } /// Uploads attachment as a multipart/form-data and returns only the uploaded remote file. @@ -26,8 +26,8 @@ public protocol CDNClient { /// - completion: Returns the uploaded file's information. func uploadAttachment( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Result) -> Void ) /// Uploads attachment as a multipart/form-data and returns the uploaded remote file and its thumbnail. @@ -37,16 +37,16 @@ public protocol CDNClient { /// - completion: Returns the uploaded file's information. func uploadAttachment( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Result) -> Void ) } public extension CDNClient { func uploadAttachment( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Result) -> Void ) { uploadAttachment(attachment, progress: progress, completion: { (result: Result) in switch result { @@ -60,14 +60,15 @@ public extension CDNClient { } /// Default implementation of CDNClient that uses Stream CDN -class StreamCDNClient: CDNClient { +final class StreamCDNClient: CDNClient, Sendable { static var maxAttachmentSize: Int64 { 100 * 1024 * 1024 } private let decoder: RequestDecoder private let encoder: RequestEncoder private let session: URLSession /// Keeps track of uploading tasks progress - @Atomic private var taskProgressObservers: [Int: NSKeyValueObservation] = [:] + private nonisolated(unsafe) var _taskProgressObservers: [Int: NSKeyValueObservation] = [:] + private let queue = DispatchQueue(label: "io.getstream.stream-cdn-client", target: .global()) init( encoder: RequestEncoder, @@ -81,8 +82,8 @@ class StreamCDNClient: CDNClient { func uploadAttachment( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)? = nil, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)? = nil, + completion: @escaping @Sendable(Result) -> Void ) { uploadAttachment(attachment, progress: progress, completion: { (result: Result) in switch result { @@ -96,8 +97,8 @@ class StreamCDNClient: CDNClient { func uploadAttachment( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)? = nil, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)? = nil, + completion: @escaping @Sendable(Result) -> Void ) { guard let uploadingState = attachment.uploadingState, @@ -148,13 +149,13 @@ class StreamCDNClient: CDNClient { if let progressListener = progress { let taskID = task.taskIdentifier - self._taskProgressObservers.mutate { observers in - observers[taskID] = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in + queue.async { + self._taskProgressObservers[taskID] = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in progressListener(progress.fractionCompleted) if progress.isFinished || progress.isCancelled { - self?._taskProgressObservers.mutate { observers in - observers[taskID]?.invalidate() - observers[taskID] = nil + self?.queue.async { [weak self] in + self?._taskProgressObservers[taskID]?.invalidate() + self?._taskProgressObservers[taskID] = nil } } } diff --git a/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift b/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift index 35c9854143c..f85016357ef 100644 --- a/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift +++ b/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift @@ -66,9 +66,9 @@ public class ChatPushNotificationInfo { } } -public class ChatRemoteNotificationHandler { - var client: ChatClient - var content: UNNotificationContent +public class ChatRemoteNotificationHandler: @unchecked Sendable { + let client: ChatClient + let content: UNNotificationContent let chatCategoryIdentifiers: Set = ["stream.chat", "MESSAGE_NEW"] let channelRepository: ChannelRepository let messageRepository: MessageRepository @@ -80,7 +80,7 @@ public class ChatRemoteNotificationHandler { messageRepository = client.messageRepository } - public func handleNotification(completion: @escaping (ChatPushNotificationContent) -> Void) -> Bool { + public func handleNotification(completion: @escaping @Sendable(ChatPushNotificationContent) -> Void) -> Bool { guard chatCategoryIdentifiers.contains(content.categoryIdentifier) else { return false } @@ -89,7 +89,7 @@ public class ChatRemoteNotificationHandler { return true } - private func getContent(completion: @escaping (ChatPushNotificationContent) -> Void) { + private func getContent(completion: @escaping @Sendable(ChatPushNotificationContent) -> Void) { guard let payload = content.userInfo["stream"], let dict = payload as? [String: String] else { return completion(.unknown(UnknownNotificationContent(content: content))) } @@ -115,7 +115,7 @@ public class ChatRemoteNotificationHandler { } } - private func getContent(cid: ChannelId, messageId: MessageId, completion: @escaping (ChatMessage?, ChatChannel?) -> Void) { + private func getContent(cid: ChannelId, messageId: MessageId, completion: @escaping @Sendable(ChatMessage?, ChatChannel?) -> Void) { var query = ChannelQuery(cid: cid, pageSize: 10, membersLimit: 10) query.options = .state channelRepository.getChannel(for: query, store: false) { [messageRepository] channelResult in diff --git a/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift index ce531036df7..452e967aa45 100644 --- a/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift +++ b/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift @@ -292,7 +292,7 @@ extension Endpoint { static func startTypingEvent(cid: ChannelId, parentMessageId: MessageId?) -> Endpoint { let eventType = EventType.userStartTyping - let body: Encodable + let body: Encodable & Sendable if let parentMessageId = parentMessageId { body = ["event": ["type": eventType.rawValue, "parent_id": parentMessageId]] } else { @@ -309,7 +309,7 @@ extension Endpoint { static func stopTypingEvent(cid: ChannelId, parentMessageId: MessageId?) -> Endpoint { let eventType = EventType.userStopTyping - let body: Encodable + let body: Encodable & Sendable if let parentMessageId = parentMessageId { body = ["event": ["type": eventType.rawValue, "parent_id": parentMessageId]] } else { diff --git a/Sources/StreamChat/APIClient/Endpoints/Endpoint.swift b/Sources/StreamChat/APIClient/Endpoints/Endpoint.swift index 2741620545d..2ea34bcc343 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Endpoint.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Endpoint.swift @@ -4,21 +4,21 @@ import Foundation -struct Endpoint: Codable { +struct Endpoint: Codable, Sendable { let path: EndpointPath let method: EndpointMethod - let queryItems: Encodable? + let queryItems: (Encodable & Sendable)? let requiresConnectionId: Bool let requiresToken: Bool - let body: Encodable? + let body: (Encodable & Sendable)? init( path: EndpointPath, method: EndpointMethod, - queryItems: Encodable? = nil, + queryItems: (Encodable & Sendable)? = nil, requiresConnectionId: Bool = false, requiresToken: Bool = true, - body: Encodable? = nil + body: (Encodable & Sendable)? = nil ) { self.path = path self.method = method @@ -64,7 +64,7 @@ struct Endpoint: Codable { } } -private extension Encodable { +private extension Encodable where Self: Sendable { func encodedAsData() throws -> Data { try JSONEncoder.stream.encode(AnyEncodable(self)) } @@ -79,7 +79,7 @@ enum EndpointMethod: String, Codable, Equatable { } /// A type representing empty response of an Endpoint. -public struct EmptyResponse: Decodable {} +public struct EmptyResponse: Decodable, Sendable {} /// A type representing empty body for `.post` Endpoints. /// Our backend currently expects a body (not `nil`), even if it's empty. diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift index 2d365f39cfb..2b3ce86560e 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift @@ -213,7 +213,7 @@ struct ChannelReadPayload: Decodable { } /// A channel config. -public class ChannelConfig: Codable { +public final class ChannelConfig: Codable, Sendable { private enum CodingKeys: String, CodingKey { case reactionsEnabled = "reactions" case typingEventsEnabled = "typing_events" diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift index 42f1fff9b22..a5a74ed8baa 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift @@ -5,7 +5,7 @@ import Foundation /// An object describing the incoming current user JSON payload. -class CurrentUserPayload: UserPayload { +final class CurrentUserPayload: UserPayload, @unchecked Sendable { /// A list of devices. let devices: [DevicePayload] /// Muted users. diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift index a94b633cfbb..2040aa42af4 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift @@ -4,7 +4,7 @@ import Foundation -class DraftPayloadResponse: Decodable { +final class DraftPayloadResponse: Decodable, Sendable { let draft: DraftPayload init(draft: DraftPayload) { @@ -12,7 +12,7 @@ class DraftPayloadResponse: Decodable { } } -class DraftListPayloadResponse: Decodable { +final class DraftListPayloadResponse: Decodable, Sendable { let drafts: [DraftPayload] let next: String? @@ -22,7 +22,7 @@ class DraftListPayloadResponse: Decodable { } } -class DraftPayload: Decodable { +final class DraftPayload: Decodable, Sendable { let cid: ChannelId? let channelPayload: ChannelDetailPayload? let createdAt: Date @@ -61,7 +61,7 @@ class DraftPayload: Decodable { } } -class DraftMessagePayload: Decodable { +final class DraftMessagePayload: Decodable, Sendable { let id: String let text: String let command: String? @@ -113,7 +113,7 @@ class DraftMessagePayload: Decodable { } } -class DraftMessageRequestBody: Encodable { +final class DraftMessageRequestBody: Encodable, Sendable { let id: String let text: String let command: String? diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessageModerationDetailsPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessageModerationDetailsPayload.swift index 9078b2c56ef..dca9205a6c4 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessageModerationDetailsPayload.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessageModerationDetailsPayload.swift @@ -4,7 +4,7 @@ import Foundation -struct MessageModerationDetailsPayload: Decodable { +struct MessageModerationDetailsPayload: Decodable, Sendable { let originalText: String let action: String let textHarms: [String]? diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift index 880be5f98ac..f49da891ef2 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift @@ -70,7 +70,7 @@ struct MessageSearchResultsPayload: Decodable { } /// An object describing the incoming message JSON payload. -class MessagePayload: Decodable { +final class MessagePayload: Decodable, Sendable { let id: String /// Only messages from `translate` endpoint contain `cid` let cid: ChannelId? @@ -103,16 +103,16 @@ class MessagePayload: Decodable { let translations: [TranslationLanguage: String]? let originalLanguage: String? let moderationDetails: MessageModerationDetailsPayload? // moderation v1 payload - var moderation: MessageModerationDetailsPayload? // moderation v2 payload + let moderation: MessageModerationDetailsPayload? // moderation v2 payload - var pinned: Bool - var pinnedBy: UserPayload? - var pinnedAt: Date? - var pinExpires: Date? + let pinned: Bool + let pinnedBy: UserPayload? + let pinnedAt: Date? + let pinExpires: Date? - var poll: PollPayload? + let poll: PollPayload? - var draft: DraftPayload? + let draft: DraftPayload? /// Only message payload from `getMessage` endpoint contains channel data. It's a convenience workaround for having to /// make an extra call do get channel details. @@ -263,7 +263,7 @@ class MessagePayload: Decodable { } /// An object describing the outgoing message JSON payload. -struct MessageRequestBody: Encodable { +struct MessageRequestBody: Encodable, Sendable { let id: String let user: UserRequestBody let text: String @@ -366,7 +366,7 @@ struct MessageReactionsPayload: Decodable { } /// A command in a message, e.g. /giphy. -public struct Command: Codable, Hashable { +public struct Command: Codable, Hashable, Sendable { /// A command name. public let name: String /// A description. diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/RawJSON.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/RawJSON.swift index 9b5244b289e..af77a2f7c91 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/RawJSON.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/RawJSON.swift @@ -7,7 +7,7 @@ import Foundation /// A `RawJSON` type. The type used for handling extra data. /// Used to store and operate objects of unknown structure that's not possible to decode. /// https://forums.swift.org/t/new-unevaluated-type-for-decoder-to-allow-later-re-encoding-of-data-with-unknown-structure/11117 -public indirect enum RawJSON: Codable, Hashable { +public indirect enum RawJSON: Codable, Hashable, Sendable { case number(Double) case string(String) case bool(Bool) diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift index 8210005270c..fa93278602e 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift @@ -34,7 +34,7 @@ enum UserPayloadsCodingKeys: String, CodingKey, CaseIterable { // MARK: - GET users /// An object describing the incoming user JSON payload. -class UserPayload: Decodable { +class UserPayload: Decodable, @unchecked Sendable { let id: String let name: String? let imageURL: URL? @@ -118,7 +118,7 @@ class UserPayload: Decodable { } /// An object describing the outgoing user JSON payload. -class UserRequestBody: Encodable { +final class UserRequestBody: Encodable, Sendable { let id: String let name: String? let imageURL: URL? diff --git a/Sources/StreamChat/APIClient/Endpoints/Requests/CustomEventRequestBody.swift b/Sources/StreamChat/APIClient/Endpoints/Requests/CustomEventRequestBody.swift index 11ab1791110..86014f3628c 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Requests/CustomEventRequestBody.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Requests/CustomEventRequestBody.swift @@ -15,3 +15,5 @@ struct CustomEventRequestBody: Encodable { try json.encode(to: encoder) } } + +extension CustomEventRequestBody: Sendable where Payload: Sendable {} diff --git a/Sources/StreamChat/APIClient/RequestDecoder.swift b/Sources/StreamChat/APIClient/RequestDecoder.swift index fd0f34da3de..07f9dda9b6c 100644 --- a/Sources/StreamChat/APIClient/RequestDecoder.swift +++ b/Sources/StreamChat/APIClient/RequestDecoder.swift @@ -5,7 +5,7 @@ import Foundation /// An object responsible for handling incoming URL request response and decoding it. -protocol RequestDecoder { +protocol RequestDecoder: Sendable { /// Decodes an incoming URL request response. /// /// - Parameters: @@ -86,11 +86,11 @@ struct DefaultRequestDecoder: RequestDecoder { } extension ClientError { - final class ExpiredToken: ClientError {} - final class RefreshingToken: ClientError {} - final class TokenRefreshed: ClientError {} - final class ConnectionError: ClientError {} - final class ResponseBodyEmpty: ClientError { + final class ExpiredToken: ClientError, @unchecked Sendable {} + final class RefreshingToken: ClientError, @unchecked Sendable {} + final class TokenRefreshed: ClientError, @unchecked Sendable {} + final class ConnectionError: ClientError, @unchecked Sendable {} + final class ResponseBodyEmpty: ClientError, @unchecked Sendable { override var localizedDescription: String { "Response body is empty." } } diff --git a/Sources/StreamChat/APIClient/RequestEncoder.swift b/Sources/StreamChat/APIClient/RequestEncoder.swift index bb6ac26b3d7..7490a31487d 100644 --- a/Sources/StreamChat/APIClient/RequestEncoder.swift +++ b/Sources/StreamChat/APIClient/RequestEncoder.swift @@ -5,7 +5,7 @@ import Foundation /// On object responsible for creating a `URLRequest`, and encoding all required and `Endpoint` specific data to it. -protocol RequestEncoder { +protocol RequestEncoder: Sendable { /// A delegate the encoder uses for obtaining the current `connectionId`. /// /// Trying to encode an `Endpoint` with the `requiresConnectionId` set to `true` without setting the delegate @@ -19,7 +19,7 @@ protocol RequestEncoder { /// - completion: Called when the encoded `URLRequest` is ready. Called with en `Error` if the encoding fails. func encodeRequest( for endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) /// Creates a new `RequestEncoder`. @@ -45,7 +45,7 @@ extension RequestEncoder { subsystems: .httpRequests ) - var result: Result = .failure( + nonisolated(unsafe) var result: Result = .failure( ClientError("Unexpected error. The result was not changed after encoding the request.") ) @@ -70,7 +70,7 @@ extension RequestEncoder { } /// The default implementation of `RequestEncoder`. -class DefaultRequestEncoder: RequestEncoder { +final class DefaultRequestEncoder: RequestEncoder, Sendable { let baseURL: URL let apiKey: APIKey @@ -83,11 +83,18 @@ class DefaultRequestEncoder: RequestEncoder { /// On the other hand, any big number for a timeout here would be "to much". In normal situations, the requests should be back in less than a second, /// otherwise we have a connection problem, which is handled as described above. private let waiterTimeout: TimeInterval = 10 - weak var connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate? + private let queue = DispatchQueue(label: "io.getstream.default-request-encoder", target: .global(qos: .userInitiated)) + + var connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate? { + get { queue.sync { _connectionDetailsProviderDelegate } } + set { queue.sync { _connectionDetailsProviderDelegate = newValue } } + } + + nonisolated(unsafe) private weak var _connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate? func encodeRequest( for endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { var request: URLRequest @@ -135,7 +142,7 @@ class DefaultRequestEncoder: RequestEncoder { private func addAuthorizationHeader( request: URLRequest, endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { guard endpoint.requiresToken else { var updatedRequest = request @@ -175,7 +182,7 @@ class DefaultRequestEncoder: RequestEncoder { private func addConnectionIdIfNeeded( request: URLRequest, endpoint: Endpoint, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { guard endpoint.requiresConnectionId else { completion(.success(request)) @@ -242,7 +249,7 @@ class DefaultRequestEncoder: RequestEncoder { } } - private func encodeJSONToQueryItems(request: inout URLRequest, data: Encodable) throws { + private func encodeJSONToQueryItems(request: inout URLRequest, data: Encodable & Sendable) throws { let data = try (data as? Data) ?? JSONEncoder.stream.encode(AnyEncodable(data)) guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw ClientError.InvalidJSON("Data is not a valid JSON: \(String(data: data, encoding: .utf8) ?? "nil")") @@ -299,12 +306,12 @@ private extension URL { typealias WaiterToken = String protocol ConnectionDetailsProviderDelegate: AnyObject { - func provideConnectionId(timeout: TimeInterval, completion: @escaping (Result) -> Void) - func provideToken(timeout: TimeInterval, completion: @escaping (Result) -> Void) + func provideConnectionId(timeout: TimeInterval, completion: @escaping @Sendable(Result) -> Void) + func provideToken(timeout: TimeInterval, completion: @escaping @Sendable(Result) -> Void) } public extension ClientError { - final class InvalidURL: ClientError {} - final class InvalidJSON: ClientError {} - final class MissingConnectionId: ClientError {} + final class InvalidURL: ClientError, @unchecked Sendable {} + final class InvalidJSON: ClientError, @unchecked Sendable {} + final class MissingConnectionId: ClientError, @unchecked Sendable {} } diff --git a/Sources/StreamChat/Audio/Analysis/AudioAnalysing.swift b/Sources/StreamChat/Audio/Analysis/AudioAnalysing.swift index 82f5699d91f..0681700194b 100644 --- a/Sources/StreamChat/Audio/Analysis/AudioAnalysing.swift +++ b/Sources/StreamChat/Audio/Analysis/AudioAnalysing.swift @@ -7,7 +7,7 @@ import AVFoundation /// Describes an object that given an `AudioAnalysisContext` will analyse and process it in order to /// generate a set of data points that describe some characteristics of the audio track provided from the /// context. -protocol AudioAnalysing { +protocol AudioAnalysing: Sendable { /// Analyse and process the provided context and provide data points limited to the number of the /// targetSamples. /// - Parameters: @@ -26,7 +26,7 @@ final class StreamAudioWaveformAnalyser: AudioAnalysing { private let audioSamplesExtractor: AudioSamplesExtractor private let audioSamplesProcessor: AudioSamplesProcessor private let audioSamplesPercentageNormaliser: AudioValuePercentageNormaliser - private let outputSettings: [String: Any] + nonisolated(unsafe) private let outputSettings: [String: Any] init( audioSamplesExtractor: AudioSamplesExtractor, @@ -148,7 +148,7 @@ final class StreamAudioWaveformAnalyser: AudioAnalysing { // MARK: - Errors -final class AudioAnalysingError: ClientError { +final class AudioAnalysingError: ClientError, @unchecked Sendable { /// Failed to read the asset provided by the `AudioAnalysisContext` static func failedToReadAsset(file: StaticString = #file, line: UInt = #line) -> AudioAnalysingError { .init("Failed to read AVAsset.", file, line) diff --git a/Sources/StreamChat/Audio/Analysis/AudioAnalysisEngine.swift b/Sources/StreamChat/Audio/Analysis/AudioAnalysisEngine.swift index 689d4704a36..8522ce71aef 100644 --- a/Sources/StreamChat/Audio/Analysis/AudioAnalysisEngine.swift +++ b/Sources/StreamChat/Audio/Analysis/AudioAnalysisEngine.swift @@ -5,7 +5,7 @@ import AVFoundation /// An object responsible to coordinate the audio analysis pipeline -public struct AudioAnalysisEngine { +public struct AudioAnalysisEngine: Sendable { /// The loader that will be called to when loading asset properties is required private let assetPropertiesLoader: AssetPropertyLoading @@ -53,7 +53,7 @@ public struct AudioAnalysisEngine { public func waveformVisualisation( fromAudioURL audioURL: URL, for targetSamples: Int, - completionHandler: @escaping (Result<[Float], Error>) -> Void + completionHandler: @escaping @Sendable(Result<[Float], Error>) -> Void ) { let asset = AVURLAsset( url: audioURL, @@ -112,7 +112,7 @@ public struct AudioAnalysisEngine { // MARK: - Errors -public final class AudioAnalysisEngineError: ClientError { +public final class AudioAnalysisEngineError: ClientError, @unchecked Sendable { /// An error occurred when the Audio track cannot be loaded from the AudioFile provided. public static func failedToLoadAVAssetTrack(file: StaticString = #file, line: UInt = #line) -> AudioAnalysisEngineError { .init("Failed to load AVAssetTrack.", file, line) diff --git a/Sources/StreamChat/Audio/Analysis/AudioSamplesExtractor.swift b/Sources/StreamChat/Audio/Analysis/AudioSamplesExtractor.swift index ee2c46dd009..d69ded2bbad 100644 --- a/Sources/StreamChat/Audio/Analysis/AudioSamplesExtractor.swift +++ b/Sources/StreamChat/Audio/Analysis/AudioSamplesExtractor.swift @@ -8,7 +8,7 @@ import AVFoundation /// of samples to process based on the provided downsamplingRate. /// /// - Note: Audio samples are expected to be stored in Int16 format. -internal class AudioSamplesExtractor { +internal class AudioSamplesExtractor: @unchecked Sendable { /// A struct to represent the result of the extractSamples method struct Result: Equatable { var samplesToProcess, downSampledLength: Int } diff --git a/Sources/StreamChat/Audio/Analysis/AudioSamplesProcessor.swift b/Sources/StreamChat/Audio/Analysis/AudioSamplesProcessor.swift index 83e28d7acd1..df620c51e95 100644 --- a/Sources/StreamChat/Audio/Analysis/AudioSamplesProcessor.swift +++ b/Sources/StreamChat/Audio/Analysis/AudioSamplesProcessor.swift @@ -6,7 +6,7 @@ import Accelerate import AVFoundation /// An object with the purpose to prepare the provided audio data for visualisation or further processing. -internal class AudioSamplesProcessor { +class AudioSamplesProcessor: @unchecked Sendable { let noiseFloor: Float /// Creates a new instances with the desired noiseFloor value diff --git a/Sources/StreamChat/Audio/Analysis/AudioValuePercentageNormaliser.swift b/Sources/StreamChat/Audio/Analysis/AudioValuePercentageNormaliser.swift index c7e68a4632d..fba466ba85f 100644 --- a/Sources/StreamChat/Audio/Analysis/AudioValuePercentageNormaliser.swift +++ b/Sources/StreamChat/Audio/Analysis/AudioValuePercentageNormaliser.swift @@ -5,7 +5,7 @@ import Foundation /// The normaliser computes the percentage values or value of the provided array or value. -internal class AudioValuePercentageNormaliser { +class AudioValuePercentageNormaliser: @unchecked Sendable { internal let valueRange: ClosedRange = -50...0 /// Compute the range between the min and max values diff --git a/Sources/StreamChat/Audio/AssetPropertyLoading.swift b/Sources/StreamChat/Audio/AssetPropertyLoading.swift index 71ff0b03453..4608089f4a6 100644 --- a/Sources/StreamChat/Audio/AssetPropertyLoading.swift +++ b/Sources/StreamChat/Audio/AssetPropertyLoading.swift @@ -71,7 +71,7 @@ public struct AssetPropertyLoadingCompositeError: Error { } /// Defines a type that represents the properties of an asset that can be loaded -public struct AssetProperty: CustomStringConvertible { +public struct AssetProperty: CustomStringConvertible, Sendable { /// The property's name public var name: String @@ -84,7 +84,7 @@ public struct AssetProperty: CustomStringConvertible { } /// A protocol that describes an object that can be used to load properties from an AVAsset -public protocol AssetPropertyLoading { +public protocol AssetPropertyLoading: Sendable { /// A method that loads the property of an AVAsset asynchronously and /// returns a result through a completion handler /// - Parameters: @@ -94,7 +94,7 @@ public protocol AssetPropertyLoading { func loadProperties( _ properties: [AssetProperty], of asset: Asset, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) } @@ -105,16 +105,19 @@ public struct StreamAssetPropertyLoader: AssetPropertyLoading { public func loadProperties( _ properties: [AssetProperty], of asset: Asset, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { // it's worth noting here that according to the documentation, the completion // handler will be invoked only once, regardless of the number of // properties we are loading. // https://developer.apple.com/documentation/avfoundation/avasynchronouskeyvalueloading/1387321-loadvaluesasynchronously + + // On iOS 15 we should switch to async load methods + nonisolated(unsafe) let unsafeAsset = asset asset.loadValuesAsynchronously(forKeys: properties.map(\.name)) { handlePropertiesLoadingResult( properties, - of: asset, + of: unsafeAsset, completion: completion ) } diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContext.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContext.swift index b77fdf9b95a..e5def1c4a93 100644 --- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContext.swift +++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContext.swift @@ -5,7 +5,7 @@ import Foundation /// A struct that represents the current state of an audio player -public struct AudioPlaybackContext: Equatable { +public struct AudioPlaybackContext: Equatable, Sendable { public var assetLocation: URL? /// The duration of the audio track in seconds diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContextAccessor.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContextAccessor.swift index 318a89298e9..2409973e4d1 100644 --- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContextAccessor.swift +++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContextAccessor.swift @@ -5,7 +5,7 @@ import Foundation /// Provides thread-safe access to the value's storage -final class AudioPlaybackContextAccessor { +final class AudioPlaybackContextAccessor: @unchecked Sendable { /// The queue that thread-safe access to the value's storage private var accessQueue: DispatchQueue diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackRate.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackRate.swift index 8808eecb84c..e51ebf14b0e 100644 --- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackRate.swift +++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackRate.swift @@ -5,7 +5,7 @@ import Foundation /// Defines a struct that describes the audio playback rate with a raw value of Float -public struct AudioPlaybackRate: Comparable, Equatable { +public struct AudioPlaybackRate: Comparable, Equatable, Sendable { public let rawValue: Float public init(rawValue: Float) { diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackState.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackState.swift index 122e952835e..0e192a06b37 100644 --- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackState.swift +++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackState.swift @@ -5,7 +5,7 @@ import Foundation /// Defines an struct which describes an audio player's playback state -public struct AudioPlaybackState: Equatable, CustomStringConvertible { +public struct AudioPlaybackState: Equatable, CustomStringConvertible, Sendable { /// The name that describes the state public let name: String diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlayerObserving.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlayerObserving.swift index b4514071cd9..e466d23c4d1 100644 --- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlayerObserving.swift +++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlayerObserving.swift @@ -14,7 +14,7 @@ protocol AudioPlayerObserving { /// - block: The block to call once a `timeControlStatus` update occurs func addTimeControlStatusObserver( _ player: AVPlayer, - using block: @escaping (AVPlayer.TimeControlStatus?) -> Void + using block: @escaping @Sendable(AVPlayer.TimeControlStatus?) -> Void ) /// Registers an observer that will periodically invoke the given block during playback to @@ -28,7 +28,7 @@ protocol AudioPlayerObserving { _ player: AVPlayer, forInterval interval: CMTime, queue: DispatchQueue?, - using block: @escaping () -> Void + using block: @escaping @Sendable() -> Void ) /// Registers and observer that will be called once the playback of an item stops @@ -37,7 +37,7 @@ protocol AudioPlayerObserving { /// - block: The block to call once a player's item has stopped func addStoppedPlaybackObserver( queue: OperationQueue?, - using block: @escaping (AVPlayerItem) -> Void + using block: @escaping @Sendable(AVPlayerItem) -> Void ) } @@ -86,7 +86,7 @@ final class StreamPlayerObserver: AudioPlayerObserving { func addTimeControlStatusObserver( _ player: AVPlayer, - using block: @escaping (AVPlayer.TimeControlStatus?) -> Void + using block: @escaping @Sendable(AVPlayer.TimeControlStatus?) -> Void ) { timeControlStatusObserver = player.observe( \.timeControlStatus, @@ -98,7 +98,7 @@ final class StreamPlayerObserver: AudioPlayerObserving { _ player: AVPlayer, forInterval interval: CMTime, queue: DispatchQueue?, - using block: @escaping () -> Void + using block: @escaping @Sendable() -> Void ) { periodicTimeObservationToken = player.addPeriodicTimeObserver( forInterval: interval, @@ -118,7 +118,7 @@ final class StreamPlayerObserver: AudioPlayerObserving { func addStoppedPlaybackObserver( queue: OperationQueue?, - using block: @escaping (AVPlayerItem) -> Void + using block: @escaping @Sendable(AVPlayerItem) -> Void ) { stoppedPlaybackObservationToken = notificationCenter.addObserver( forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift index d1879d7aae6..4b8f9bf638e 100644 --- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift +++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift @@ -6,7 +6,7 @@ import AVFoundation import Foundation /// A protocol describing an object that can be manage the playback of an audio file or stream. -public protocol AudioPlaying: AnyObject { +public protocol AudioPlaying: AnyObject, Sendable { init() /// Subscribes the provided object on AudioPlayer's updates @@ -41,7 +41,7 @@ public protocol AudioPlaying: AnyObject { } /// An implementation of ``AudioPlaying`` that can be used to stream audio files from a URL -open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { +open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate, @unchecked Sendable { // MARK: - Properties /// Provides thread-safe access to context storage @@ -131,7 +131,9 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { open func play() { do { try audioSessionConfigurator.activatePlaybackSession() - player.play() + MainActor.ensureIsolated { + player.play() + } } catch { log.error(error) stop() @@ -139,7 +141,9 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { } open func pause() { - player.pause() + MainActor.ensureIsolated { + player.pause() + } } open func stop() { @@ -166,7 +170,9 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { } open func updateRate(_ newRate: AudioPlaybackRate) { - player.rate = newRate.rawValue + MainActor.ensureIsolated { + player.rate = newRate.rawValue + } } open func seek(to time: TimeInterval) { @@ -196,13 +202,14 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { // MARK: - Helpers func playbackWillStop(_ playerItem: AVPlayerItem) { - guard - let playerItemURL = (playerItem.asset as? AVURLAsset)?.url, - let currentItemURL = (player.currentItem?.asset as? AVURLAsset)?.url, - playerItemURL == currentItemURL - else { - return - } + let matchesCurrentItem: Bool = { + MainActor.ensureIsolated { + let playerItemURL = (playerItem.asset as? AVURLAsset)?.url + let currentItemURL = (player.currentItem?.asset as? AVURLAsset)?.url + return playerItemURL != nil && playerItemURL == currentItemURL + } + }() + guard matchesCurrentItem else { return } updateContext { value in value.state = .stopped value.currentTime = 0 @@ -229,7 +236,7 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { guard let self = self, self.context.isSeeking == false else { return } - + let rate = MainActor.ensureIsolated { player.rate } self.updateContext { value in let currentTime = player.currentTime().seconds value.currentTime = currentTime.isFinite && !currentTime.isNaN @@ -238,7 +245,7 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { value.isSeeking = false - value.rate = .init(rawValue: player.rate) + value.rate = .init(rawValue: rate) } } @@ -274,7 +281,9 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { } private func notifyDelegates() { - multicastDelegate.invoke { $0.audioPlayer(self, didUpdateContext: context) } + MainActor.ensureIsolated { + multicastDelegate.invoke { $0.audioPlayer(self, didUpdateContext: context) } + } } /// Provides thread-safe updates for the player's context and makes sure to forward any updates @@ -299,7 +308,8 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { /// We are going to check if the URL requested to load, represents the currentItem that we /// have already loaded (if any). In this case, we will try either to resume the existing playback /// or restart it, if possible. - if let currentItem = player.currentItem?.asset as? AVURLAsset, + let currentItem = MainActor.ensureIsolated { player.currentItem?.asset as? AVURLAsset } + if let currentItem, asset.url == currentItem.url, context.assetLocation == asset.url { /// If the currentItem is paused, we want to continue the playback diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioQueuePlayer.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioQueuePlayer.swift index f51c70c6a15..39dc484ba57 100644 --- a/Sources/StreamChat/Audio/AudioPlayer/AudioQueuePlayer.swift +++ b/Sources/StreamChat/Audio/AudioPlayer/AudioQueuePlayer.swift @@ -19,7 +19,7 @@ public protocol AudioQueuePlayerDatasource: AnyObject { ) -> URL? } -open class StreamAudioQueuePlayer: StreamAudioPlayer { +open class StreamAudioQueuePlayer: StreamAudioPlayer, @unchecked Sendable { open weak var dataSource: AudioQueuePlayerDatasource? override open func playbackWillStop(_ playerItem: AVPlayerItem) { diff --git a/Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift b/Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift index 586bd9a845f..f0f4c5b82f8 100644 --- a/Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift +++ b/Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift @@ -22,7 +22,7 @@ public protocol AudioRecording { /// - Note: If the recording permission has been answered before /// the completionHandler will be called immediately, otherwise it will be called once the user has /// replied on the request permission prompt. - func beginRecording(_ completionHandler: @escaping (() -> Void)) + func beginRecording(_ completionHandler: @escaping @Sendable() -> Void) /// Pause the currently active recording process func pauseRecording() @@ -37,7 +37,7 @@ public protocol AudioRecording { // MARK: - Implementation /// Definition of a class to handle audio recording -open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegate, AppStateObserverDelegate { +open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegate, AppStateObserverDelegate, @unchecked Sendable { /// Contains the configuration properties required by the AudioRecorder public struct Configuration { /// The settings that will be used to create **internally** the AVAudioRecorder instances @@ -79,7 +79,7 @@ open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegat } /// The default Configuration that is being bused by `StreamAudioRecorder` - public static let `default` = Configuration( + nonisolated(unsafe) public static let `default` = Configuration( audioRecorderSettings: [ AVFormatIDKey: Int(kAudioFormatMPEG4AAC), AVSampleRateKey: 12000, @@ -198,7 +198,7 @@ open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegat multicastDelegate.add(additionalDelegate: subscriber) } - open func beginRecording(_ completionHandler: @escaping (() -> Void)) { + open func beginRecording(_ completionHandler: @escaping @Sendable() -> Void) { do { /// Enable recording on `AudioSession` try audioSessionConfigurator.activateRecordingSession() @@ -483,7 +483,7 @@ open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegat // MARK: - Error /// An enum that acts as a namespace for various audio recording errors that might occur -public final class AudioRecorderError: ClientError { +public final class AudioRecorderError: ClientError, @unchecked Sendable { /// An unknown error occurred public static func unknown(file: StaticString = #file, line: UInt = #line) -> AudioRecorderError { .init("An unknown error occurred.", file, line) } diff --git a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContext.swift b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContext.swift index 58b6e91f5b2..75752f6132a 100644 --- a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContext.swift +++ b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContext.swift @@ -5,7 +5,7 @@ import Foundation /// A struct that represents the current state of an audio recording session -public struct AudioRecordingContext: Hashable { +public struct AudioRecordingContext: Hashable, Sendable { /// The current state of the audio recording session public var state: AudioRecordingState diff --git a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContextAccessor.swift b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContextAccessor.swift index 5a8330e91ed..256ce6415f3 100644 --- a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContextAccessor.swift +++ b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContextAccessor.swift @@ -5,11 +5,11 @@ import Foundation /// Provides thread-safe access to the value's storage -final class AudioRecordingContextAccessor { +final class AudioRecordingContextAccessor: Sendable { /// The queue that thread-safe access to the value's storage - private var accessQueue: DispatchQueue + private let accessQueue: DispatchQueue - private var _value: AudioRecordingContext + nonisolated(unsafe) private var _value: AudioRecordingContext var value: AudioRecordingContext { get { readValue() } set { writeValue(newValue) } diff --git a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingState.swift b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingState.swift index 9f469bb3b51..f057130ddd8 100644 --- a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingState.swift +++ b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingState.swift @@ -6,7 +6,7 @@ import Foundation /// Defines a struct named AudioRecordingState that is being used to describe the state of a recording /// session. -public struct AudioRecordingState: Hashable { +public struct AudioRecordingState: Hashable, Sendable { var rawValue: String /// Defines a static constant property called recording of type AudioRecordingState with a raw value diff --git a/Sources/StreamChat/Audio/AudioSessionConfiguring.swift b/Sources/StreamChat/Audio/AudioSessionConfiguring.swift index 3c10f6f6831..3f8ef28d4fc 100644 --- a/Sources/StreamChat/Audio/AudioSessionConfiguring.swift +++ b/Sources/StreamChat/Audio/AudioSessionConfiguring.swift @@ -27,7 +27,7 @@ public protocol AudioSessionConfiguring { /// with a result, call the completionHandler to continue the flow. /// - Parameter completionHandler: The completion handler that will be called to continue the flow. func requestRecordPermission( - _ completionHandler: @escaping (Bool) -> Void + _ completionHandler: @escaping @Sendable(Bool) -> Void ) } @@ -46,10 +46,10 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring { public func deactivatePlaybackSession() throws { /* No-op */ } - public func requestRecordPermission(_ completionHandler: @escaping (Bool) -> Void) { completionHandler(true) } + public func requestRecordPermission(_ completionHandler: @escaping @Sendable(Bool) -> Void) { completionHandler(true) } } #else -open class StreamAudioSessionConfigurator: AudioSessionConfiguring { +open class StreamAudioSessionConfigurator: AudioSessionConfiguring, @unchecked Sendable { /// The audioSession with which the configurator will interact. private let audioSession: AudioSessionProtocol @@ -120,7 +120,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring { /// with a response. /// - Note: The closure's invocation will be dispatched on the MainThread. open func requestRecordPermission( - _ completionHandler: @escaping (Bool) -> Void + _ completionHandler: @escaping @Sendable(Bool) -> Void ) { audioSession.requestRecordPermission { [weak self] in self?.handleRecordPermissionResponse($0, completionHandler: completionHandler) @@ -141,7 +141,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring { private func handleRecordPermissionResponse( _ permissionGranted: Bool, - completionHandler: @escaping (Bool) -> Void + completionHandler: @escaping @Sendable(Bool) -> Void ) { guard Thread.isMainThread else { DispatchQueue.main.async { [weak self] in @@ -178,7 +178,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring { // MARK: - Errors -final class AudioSessionConfiguratorError: ClientError { +final class AudioSessionConfiguratorError: ClientError, @unchecked Sendable { /// An unknown error occurred static func noAvailableInputsFound( file: StaticString = #file, diff --git a/Sources/StreamChat/ChatClient+Environment.swift b/Sources/StreamChat/ChatClient+Environment.swift index 3fa6d723ce6..25c6c9c59ff 100644 --- a/Sources/StreamChat/ChatClient+Environment.swift +++ b/Sources/StreamChat/ChatClient+Environment.swift @@ -6,16 +6,24 @@ import Foundation extension ChatClient { /// An object containing all dependencies of `Client` - struct Environment { - var apiClientBuilder: ( + struct Environment: Sendable { + var apiClientBuilder: @Sendable( _ sessionConfiguration: URLSessionConfiguration, _ requestEncoder: RequestEncoder, _ requestDecoder: RequestDecoder, _ attachmentDownloader: AttachmentDownloader, _ attachmentUploader: AttachmentUploader - ) -> APIClient = APIClient.init + ) -> APIClient = { + APIClient( + sessionConfiguration: $0, + requestEncoder: $1, + requestDecoder: $2, + attachmentDownloader: $3, + attachmentUploader: $4 + ) + } - var webSocketClientBuilder: (( + var webSocketClientBuilder: (@Sendable( _ sessionConfiguration: URLSessionConfiguration, _ requestEncoder: RequestEncoder, _ eventDecoder: AnyEventDecoder, @@ -29,7 +37,7 @@ extension ChatClient { ) } - var databaseContainerBuilder: ( + var databaseContainerBuilder: @Sendable( _ kind: DatabaseContainer.Kind, _ chatClientConfig: ChatClientConfig ) -> DatabaseContainer = { @@ -39,19 +47,19 @@ extension ChatClient { ) } - var reconnectionHandlerBuilder: (_ chatClientConfig: ChatClientConfig) -> StreamTimer? = { + var reconnectionHandlerBuilder: @Sendable(_ chatClientConfig: ChatClientConfig) -> StreamTimer? = { guard let reconnectionTimeout = $0.reconnectionTimeout else { return nil } return ScheduledStreamTimer(interval: reconnectionTimeout, fireOnStart: false, repeats: false) } - var requestEncoderBuilder: (_ baseURL: URL, _ apiKey: APIKey) -> RequestEncoder = DefaultRequestEncoder.init - var requestDecoderBuilder: () -> RequestDecoder = DefaultRequestDecoder.init + var requestEncoderBuilder: @Sendable(_ baseURL: URL, _ apiKey: APIKey) -> RequestEncoder = { DefaultRequestEncoder(baseURL: $0, apiKey: $1) } + var requestDecoderBuilder: @Sendable() -> RequestDecoder = { DefaultRequestDecoder() } - var eventDecoderBuilder: () -> EventDecoder = EventDecoder.init + var eventDecoderBuilder: @Sendable() -> EventDecoder = { EventDecoder() } - var notificationCenterBuilder = EventNotificationCenter.init + var notificationCenterBuilder: @Sendable(_ database: DatabaseContainer) -> EventNotificationCenter = { EventNotificationCenter(database: $0) } - var internetConnection: (_ center: NotificationCenter, _ monitor: InternetConnectionMonitor) -> InternetConnection = { + var internetConnection: @Sendable(_ center: NotificationCenter, _ monitor: InternetConnectionMonitor) -> InternetConnection = { InternetConnection(notificationCenter: $0, monitor: $1) } @@ -65,9 +73,23 @@ extension ChatClient { var monitor: InternetConnectionMonitor? - var connectionRepositoryBuilder = ConnectionRepository.init + var connectionRepositoryBuilder: @Sendable( + _ isClientInActiveMode: Bool, + _ syncRepository: SyncRepository, + _ webSocketClient: WebSocketClient?, + _ apiClient: APIClient, + _ timerType: Timer.Type + ) -> ConnectionRepository = { + ConnectionRepository( + isClientInActiveMode: $0, + syncRepository: $1, + webSocketClient: $2, + apiClient: $3, + timerType: $4 + ) + } - var backgroundTaskSchedulerBuilder: () -> BackgroundTaskScheduler? = { + var backgroundTaskSchedulerBuilder: @Sendable() -> BackgroundTaskScheduler? = { if Bundle.main.isAppExtension { // No background task scheduler exists for app extensions. return nil @@ -85,7 +107,7 @@ extension ChatClient { var tokenExpirationRetryStrategy: RetryStrategy = DefaultRetryStrategy() - var connectionRecoveryHandlerBuilder: ( + var connectionRecoveryHandlerBuilder: @Sendable( _ webSocketClient: WebSocketClient, _ eventNotificationCenter: EventNotificationCenter, _ syncRepository: SyncRepository, @@ -105,9 +127,23 @@ extension ChatClient { ) } - var authenticationRepositoryBuilder = AuthenticationRepository.init + var authenticationRepositoryBuilder: @Sendable( + _ apiClient: APIClient, + _ databaseContainer: DatabaseContainer, + _ connectionRepository: ConnectionRepository, + _ tokenExpirationRetryStrategy: RetryStrategy, + _ timerType: Timer.Type + ) -> AuthenticationRepository = { + AuthenticationRepository( + apiClient: $0, + databaseContainer: $1, + connectionRepository: $2, + tokenExpirationRetryStrategy: $3, + timerType: $4 + ) + } - var syncRepositoryBuilder: ( + var syncRepositoryBuilder: @Sendable( _ config: ChatClientConfig, _ offlineRequestsRepository: OfflineRequestsRepository, _ eventNotificationCenter: EventNotificationCenter, @@ -125,42 +161,42 @@ extension ChatClient { ) } - var channelRepositoryBuilder: ( + var channelRepositoryBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient ) -> ChannelRepository = { ChannelRepository(database: $0, apiClient: $1) } - var pollsRepositoryBuilder: ( + var pollsRepositoryBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient ) -> PollsRepository = { PollsRepository(database: $0, apiClient: $1) } - var draftMessagesRepositoryBuilder: ( + var draftMessagesRepositoryBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient ) -> DraftMessagesRepository = { DraftMessagesRepository(database: $0, apiClient: $1) } - var channelListUpdaterBuilder: ( + var channelListUpdaterBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient ) -> ChannelListUpdater = { ChannelListUpdater(database: $0, apiClient: $1) } - var messageRepositoryBuilder: ( + var messageRepositoryBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient ) -> MessageRepository = { MessageRepository(database: $0, apiClient: $1) } - var offlineRequestsRepositoryBuilder: ( + var offlineRequestsRepositoryBuilder: @Sendable( _ messageRepository: MessageRepository, _ database: DatabaseContainer, _ apiClient: APIClient, diff --git a/Sources/StreamChat/ChatClient.swift b/Sources/StreamChat/ChatClient.swift index 9048ea8202b..c8ecf7e1ba3 100644 --- a/Sources/StreamChat/ChatClient.swift +++ b/Sources/StreamChat/ChatClient.swift @@ -12,7 +12,7 @@ import Foundation /// case requires it (i.e. more than one window with different workspaces in a Slack-like app). /// /// - Important: When using multiple instances of `ChatClient` at the same time, it is required to use a different ``ChatClientConfig/localStorageFolderURL`` for each instance. For example, adding an additional path component to the default URL. -public class ChatClient { +public class ChatClient: @unchecked Sendable { /// The `UserId` of the currently logged in user. public var currentUserId: UserId? { authenticationRepository.currentUserId @@ -25,7 +25,7 @@ public class ChatClient { /// The app configuration settings. It is automatically fetched when `connectUser` is called. /// Can be manually refetched by calling `loadAppSettings()`. - public private(set) var appSettings: AppSettings? + @Atomic public private(set) var appSettings: AppSettings? /// The current connection status of the client. /// @@ -46,17 +46,22 @@ public class ChatClient { /// /// `ChatClient` initializes a set of background workers that keep observing the current state of the system and perform /// work if needed (i.e. when a new message pending sent appears in the database, a worker tries to send it.) - private(set) var backgroundWorkers: [Worker] = [] + @Atomic private(set) var backgroundWorkers: [Worker] = [] /// Background worker that takes care about client connection recovery when the Internet comes back OR app transitions from background to foreground. - private(set) var connectionRecoveryHandler: ConnectionRecoveryHandler? + @Atomic private(set) var connectionRecoveryHandler: ConnectionRecoveryHandler? /// The notification center used to send and receive notifications about incoming events. private(set) var eventNotificationCenter: EventNotificationCenter /// The registry that contains all the attachment payloads associated with their attachment types. /// For the meantime this is a static property to avoid breaking changes. On v5, this can be changed. - private(set) static var attachmentTypesRegistry: [AttachmentType: AttachmentPayload.Type] = [ + private(set) static var attachmentTypesRegistry: [AttachmentType: AttachmentPayload.Type] { + get { Self.queue.sync { _attachmentTypesRegistry } } + set { Self.queue.sync { _attachmentTypesRegistry = newValue } } + } + + nonisolated(unsafe) private(set) static var _attachmentTypesRegistry: [AttachmentType: AttachmentPayload.Type] = [ .image: ImageAttachmentPayload.self, .video: VideoAttachmentPayload.self, .audio: AudioAttachmentPayload.self, @@ -96,17 +101,27 @@ public class ChatClient { let databaseContainer: DatabaseContainer /// The component responsible to timeout the user connection if it takes more time than the `ChatClientConfig.reconnectionTimeout`. - var reconnectionTimeoutHandler: StreamTimer? + var reconnectionTimeoutHandler: StreamTimer? { + get { Self.queue.sync { _reconnectionTimeoutHandler } } + set { Self.queue.sync { _reconnectionTimeoutHandler = newValue } } + } + + private var _reconnectionTimeoutHandler: StreamTimer? /// The environment object containing all dependencies of this `Client` instance. private let environment: Environment - @Atomic static var activeLocalStorageURLs = Set() - /// The default configuration of URLSession to be used for both the `APIClient` and `WebSocketClient`. It contains all /// required header auth parameters to make a successful request. - private var urlSessionConfiguration: URLSessionConfiguration + private let urlSessionConfiguration: URLSessionConfiguration + static var activeLocalStorageURLs: Set { + queue.sync { _activeLocalStorageURLs } + } + + nonisolated(unsafe) private static var _activeLocalStorageURLs = Set() + private static let queue = DispatchQueue(label: "io.getstream.chat-client", target: .global()) + /// Creates a new instance of `ChatClient`. /// - Parameter config: The config object for the `Client`. See `ChatClientConfig` for all configuration options. public convenience init( @@ -229,7 +244,9 @@ public class ChatClient { } deinit { - Self._activeLocalStorageURLs.mutate { $0.subtract(databaseContainer.persistentStoreDescriptions.compactMap(\.url)) } + Self.queue.sync { + Self._activeLocalStorageURLs.subtract(databaseContainer.persistentStoreDescriptions.compactMap(\.url)) + } completeConnectionIdWaiters(connectionId: nil) completeTokenWaiters(token: nil) reconnectionTimeoutHandler?.stop() @@ -266,10 +283,10 @@ public class ChatClient { } private func validateIntegrity() { - Self._activeLocalStorageURLs.mutate { urls in - let existingCount = urls.count - urls.formUnion(databaseContainer.persistentStoreDescriptions.compactMap(\.url).filter { $0.path != "/dev/null" }) - guard existingCount == urls.count, !urls.isEmpty else { return } + Self.queue.async { [databaseContainer] in + let existingCount = Self._activeLocalStorageURLs.count + Self._activeLocalStorageURLs.formUnion(databaseContainer.persistentStoreDescriptions.compactMap(\.url).filter { $0.path != "/dev/null" }) + guard existingCount == Self._activeLocalStorageURLs.count, !Self._activeLocalStorageURLs.isEmpty else { return } log.error( """ There are multiple ChatClient instances using the same `ChatClientConfig.localStorageFolderURL` - this is disallowed. @@ -304,7 +321,7 @@ public class ChatClient { public func connectUser( userInfo: UserInfo, tokenProvider: @escaping TokenProvider, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { reconnectionTimeoutHandler?.start() connectionRecoveryHandler?.start() @@ -353,7 +370,7 @@ public class ChatClient { public func connectUser( userInfo: UserInfo, token: Token, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { guard token.expiration == nil else { let error = ClientError.MissingTokenProvider() @@ -399,7 +416,7 @@ public class ChatClient { /// - completion: The completion that will be called once the **first** user session for the given token is setup. public func connectGuestUser( userInfo: UserInfo, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { connectionRepository.initialize() connectionRecoveryHandler?.start() @@ -426,7 +443,7 @@ public class ChatClient { /// Connects an anonymous user /// - Parameter completion: The completion that will be called once the **first** user session for the given token is setup. - public func connectAnonymousUser(completion: ((Error?) -> Void)? = nil) { + public func connectAnonymousUser(completion: (@Sendable(Error?) -> Void)? = nil) { connectionRepository.initialize() reconnectionTimeoutHandler?.start() connectionRecoveryHandler?.start() @@ -464,7 +481,7 @@ public class ChatClient { /// Disconnects the chat client from the chat servers. No further updates from the servers /// are received. - public func disconnect(completion: @escaping () -> Void) { + public func disconnect(completion: @escaping @Sendable() -> Void) { connectionRepository.disconnect(source: .userInitiated) { log.info("The `ChatClient` has been disconnected.", subsystems: .webSocket) completion() @@ -587,7 +604,7 @@ public class ChatClient { /// Fetches the app settings and updates the ``ChatClient/appSettings``. /// - Parameter completion: The completion block once the app settings has finished fetching. public func loadAppSettings( - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { apiClient.request(endpoint: .appSettings()) { [weak self] result in switch result { @@ -646,7 +663,7 @@ public class ChatClient { /// Starts the process to refresh the token /// - Parameter completion: A block to be executed when the process is completed. Contains an error if something went wrong - private func refreshToken(completion: ((Error?) -> Void)?) { + private func refreshToken(completion: (@Sendable(Error?) -> Void)?) { authenticationRepository.refreshToken { completion?($0) } @@ -705,11 +722,11 @@ extension ChatClient: ConnectionStateDelegate { /// `Client` provides connection details for the `RequestEncoder`s it creates. extension ChatClient: ConnectionDetailsProviderDelegate { - func provideToken(timeout: TimeInterval = 10, completion: @escaping (Result) -> Void) { + func provideToken(timeout: TimeInterval = 10, completion: @escaping @Sendable(Result) -> Void) { authenticationRepository.provideToken(timeout: timeout, completion: completion) } - func provideConnectionId(timeout: TimeInterval = 10, completion: @escaping (Result) -> Void) { + func provideConnectionId(timeout: TimeInterval = 10, completion: @escaping @Sendable(Result) -> Void) { connectionRepository.provideConnectionId(timeout: timeout, completion: completion) } @@ -739,11 +756,11 @@ extension ChatClient { } extension ClientError { - public final class MissingLocalStorageURL: ClientError { + public final class MissingLocalStorageURL: ClientError, @unchecked Sendable { override public var localizedDescription: String { "The URL provided in ChatClientConfig is `nil`." } } - public final class ConnectionNotSuccessful: ClientError { + public final class ConnectionNotSuccessful: ClientError, @unchecked Sendable { override public var localizedDescription: String { """ Connection to the API has failed. @@ -755,7 +772,7 @@ extension ClientError { } } - public final class ReconnectionTimeout: ClientError { + public final class ReconnectionTimeout: ClientError, @unchecked Sendable { override public var localizedDescription: String { """ The reconnection process has timed out after surpassing the value from `ChatClientConfig.reconnectionTimeout`. @@ -763,10 +780,10 @@ extension ClientError { } } - public final class MissingToken: ClientError {} - final class WaiterTimeout: ClientError {} + public final class MissingToken: ClientError, @unchecked Sendable {} + final class WaiterTimeout: ClientError, @unchecked Sendable {} - public final class ClientIsNotInActiveMode: ClientError { + public final class ClientIsNotInActiveMode: ClientError, @unchecked Sendable { override public var localizedDescription: String { """ ChatClient is in connectionless mode, it cannot connect to websocket. @@ -775,7 +792,7 @@ extension ClientError { } } - public final class ConnectionWasNotInitiated: ClientError { + public final class ConnectionWasNotInitiated: ClientError, @unchecked Sendable { override public var localizedDescription: String { """ Before performing any other actions on chat client it's required to connect by using \ @@ -784,13 +801,13 @@ extension ClientError { } } - public final class ClientHasBeenDeallocated: ClientError { + public final class ClientHasBeenDeallocated: ClientError, @unchecked Sendable { override public var localizedDescription: String { "ChatClient has been deallocated, make sure to keep at least one strong reference to it." } } - public final class MissingTokenProvider: ClientError { + public final class MissingTokenProvider: ClientError, @unchecked Sendable { override public var localizedDescription: String { """ Missing token refresh provider to get a new token diff --git a/Sources/StreamChat/Config/BaseURL.swift b/Sources/StreamChat/Config/BaseURL.swift index 728e6dd3df5..ef8633bfdff 100644 --- a/Sources/StreamChat/Config/BaseURL.swift +++ b/Sources/StreamChat/Config/BaseURL.swift @@ -5,7 +5,7 @@ import Foundation /// A struct representing base URL for `ChatClient`. -public struct BaseURL: CustomStringConvertible { +public struct BaseURL: CustomStringConvertible, Sendable { /// The default base URL for StreamChat service. public static let `default` = BaseURL(urlString: "https://chat.stream-io-api.com/")! diff --git a/Sources/StreamChat/Config/ChatClientConfig.swift b/Sources/StreamChat/Config/ChatClientConfig.swift index be56244de93..3a9bd89628f 100644 --- a/Sources/StreamChat/Config/ChatClientConfig.swift +++ b/Sources/StreamChat/Config/ChatClientConfig.swift @@ -13,7 +13,7 @@ import Foundation /// config.channel.keystrokeEventTimeout = 15 /// ``` /// -public struct ChatClientConfig { +public struct ChatClientConfig: Sendable { /// The `APIKey` unique for your chat app. /// /// The API key can be obtained by registering on [our website](https://getstream.io/chat/\). @@ -155,7 +155,7 @@ public struct ChatClientConfig { public var maxAttachmentCountPerMessage = 30 /// Specifies the visibility of deleted messages. - public enum DeletedMessageVisibility: String, CustomStringConvertible, CustomDebugStringConvertible { + public enum DeletedMessageVisibility: String, CustomStringConvertible, CustomDebugStringConvertible, Sendable { /// All deleted messages are always hidden. case alwaysHidden /// Deleted message by current user are visible, other deleted messages are hidden. @@ -222,13 +222,13 @@ extension ChatClientConfig { extension ChatClientConfig { /// Advanced settings for the local caching and model serialization. - public struct LocalCaching: Equatable { + public struct LocalCaching: Equatable, Sendable { /// `ChatChannel` specific local caching and model serialization settings. public var chatChannel = ChatChannel() } /// `ChatChannel` specific local caching and model serialization settings. - public struct ChatChannel: Equatable { + public struct ChatChannel: Equatable, Sendable { /// Limit the max number of watchers included in `ChatChannel.lastActiveWatchers`. public var lastActiveWatchersLimit = 100 /// Limit the max number of members included in `ChatChannel.lastActiveMembers`. @@ -242,7 +242,7 @@ extension ChatClientConfig { /// /// An API key can be obtained by registering on [our website](https://getstream.io/chat/trial/\). /// -public struct APIKey: Equatable { +public struct APIKey: Equatable, Sendable { /// The string representation of the API key public let apiKeyString: String diff --git a/Sources/StreamChat/Config/StreamModelsTransformer.swift b/Sources/StreamChat/Config/StreamModelsTransformer.swift index 90f51650f17..bcb15e3c305 100644 --- a/Sources/StreamChat/Config/StreamModelsTransformer.swift +++ b/Sources/StreamChat/Config/StreamModelsTransformer.swift @@ -25,7 +25,7 @@ import Foundation /// ) /// } /// ``` -public protocol StreamModelsTransformer { +public protocol StreamModelsTransformer: Sendable { /// Transforms the given `ChatChannel` model. func transform(channel: ChatChannel) -> ChatChannel diff --git a/Sources/StreamChat/Config/StreamRuntimeCheck.swift b/Sources/StreamChat/Config/StreamRuntimeCheck.swift index ea8917dc7d0..187af5e28e7 100644 --- a/Sources/StreamChat/Config/StreamRuntimeCheck.swift +++ b/Sources/StreamChat/Config/StreamRuntimeCheck.swift @@ -8,8 +8,13 @@ public enum StreamRuntimeCheck { /// Enables assertions thrown by the Stream SDK. /// /// When set to false, a message will be logged on console, but the assertion will not be thrown. - public static var assertionsEnabled = false + public static var assertionsEnabled: Bool { + get { queue.sync { _assertionsEnabled } } + set { queue.async { _assertionsEnabled = newValue } } + } + nonisolated(unsafe) private static var _assertionsEnabled = false + /// For *internal use* only /// /// Established the maximum depth of relationships to fetch when performing a mapping @@ -18,7 +23,12 @@ public enum StreamRuntimeCheck { /// Relationship: Message ---> QuotedMessage ---> QuotedMessage ---X--- NIL /// Relationship: Channel ---> Message ---> QuotedMessage ---X--- NIL /// Depth: 0 1 2 3 - static var _backgroundMappingRelationshipsMaxDepth = 2 + static var _backgroundMappingRelationshipsMaxDepth: Int { + get { queue.sync { __backgroundMappingRelationshipsMaxDepth } } + set { queue.async { __backgroundMappingRelationshipsMaxDepth = newValue } } + } + + nonisolated(unsafe) private static var __backgroundMappingRelationshipsMaxDepth = 2 /// For *internal use* only /// @@ -30,5 +40,14 @@ public enum StreamRuntimeCheck { /// For *internal use* only /// /// Core Data prefetches data used for creating immutable model objects (faulting is disabled). - public static var _isDatabasePrefetchingEnabled = false + public static var _isDatabasePrefetchingEnabled: Bool { + get { queue.sync { __isDatabasePrefetchingEnabled } } + set { queue.async { __isDatabasePrefetchingEnabled = newValue } } + } + + nonisolated(unsafe) private static var __isDatabasePrefetchingEnabled = false + + // MARK: - + + private static let queue = DispatchQueue(label: "io.getstream.stream-runtime-check", target: .global()) } diff --git a/Sources/StreamChat/Config/Token.swift b/Sources/StreamChat/Config/Token.swift index cc73ff0452e..11c797c9d92 100644 --- a/Sources/StreamChat/Config/Token.swift +++ b/Sources/StreamChat/Config/Token.swift @@ -5,7 +5,7 @@ import Foundation /// The type is designed to store the JWT and the user it is related to. -public struct Token: Decodable, Equatable, ExpressibleByStringLiteral { +public struct Token: Decodable, Equatable, ExpressibleByStringLiteral, Sendable { public let rawValue: String public let userId: UserId public let expiration: Date? @@ -83,7 +83,7 @@ public extension Token { } extension ClientError { - public final class InvalidToken: ClientError {} + public final class InvalidToken: ClientError, @unchecked Sendable {} } private extension String { diff --git a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift index 396c055a51f..42b071b7377 100644 --- a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift +++ b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift @@ -11,14 +11,14 @@ import Foundation /// getting new messages in the channel), and for quick channel mutations (like adding a member to a channel). /// /// - Note: For an async-await alternative of the `ChatChannelController`, please check ``Chat`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class ChatChannelController: DataController, DelegateCallable, DataStoreProvider { +public class ChatChannelController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The ChannelQuery this controller observes. @Atomic public private(set) var channelQuery: ChannelQuery - + /// The channel list query the channel is related to. /// It's `nil` when this controller wasn't created by a `ChannelListController` public let channelListQuery: ChannelListQuery? - + /// Flag indicating whether channel is created on backend. We need this flag to restrict channel modification requests /// before channel is created on backend. /// There are 2 ways of creating new channel: @@ -28,7 +28,12 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// In this case `cid` on `channelQuery `will be valid but all channel modifications will /// fail because channel with provided `id` will be missing on backend side. /// That is why we need to check both flag and valid `cid` before modifications. - private var isChannelAlreadyCreated: Bool + private var isChannelAlreadyCreated: Bool { + get { queue.sync { _isChannelAlreadyCreated } } + set { queue.sync { _isChannelAlreadyCreated = newValue } } + } + + private var _isChannelAlreadyCreated: Bool /// The identifier of a channel this controller observes. /// Will be `nil` when we want to create direct message channel and `id` @@ -70,16 +75,9 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP private let channelMemberUpdater: ChannelMemberUpdater - private lazy var eventSender: TypingEventsSender = self.environment.eventSenderBuilder( - client.databaseContainer, - client.apiClient - ) + private let eventSender: TypingEventsSender - private lazy var readStateHandler: ReadStateHandler = self.environment.readStateHandlerBuilder( - client.authenticationRepository, - updater, - client.messageRepository - ) + private let readStateHandler: ReadStateHandler /// A Boolean value that returns whether the oldest messages have all been loaded or not. public var hasLoadedAllPreviousMessages: Bool { @@ -148,7 +146,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// A boolean value indicating if it should send typing events. /// It is `true` if the channel typing events are enabled as well as the user privacy settings. - func shouldSendTypingEvents(completion: @escaping (Bool) -> Void) { + func shouldSendTypingEvents(completion: @escaping @Sendable(Bool) -> Void) { guard channel?.canSendTypingEvents ?? true else { completion(false) return @@ -182,14 +180,19 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP private var channelObserver: BackgroundEntityDatabaseObserver? private var messagesObserver: BackgroundListDatabaseObserver? - private var eventObservers: [EventObserver] = [] + private var eventObservers: [EventObserver] { + get { queue.sync { _eventObservers } } + set { queue.sync { _eventObservers = newValue } } + } + + private var _eventObservers: [EventObserver] = [] private let environment: Environment private let pollsRepository: PollsRepository private let draftsRepository: DraftMessagesRepository - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -220,8 +223,12 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP self.channelListQuery = channelListQuery self.client = client self.environment = environment - self.isChannelAlreadyCreated = isChannelAlreadyCreated + _isChannelAlreadyCreated = isChannelAlreadyCreated self.messageOrdering = messageOrdering + eventSender = environment.eventSenderBuilder( + client.databaseContainer, + client.apiClient + ) updater = self.environment.channelUpdaterBuilder( client.channelRepository, client.messageRepository, @@ -229,6 +236,11 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP client.databaseContainer, client.apiClient ) + readStateHandler = environment.readStateHandlerBuilder( + client.authenticationRepository, + updater, + client.messageRepository + ) channelMemberUpdater = self.environment.memberUpdaterBuilder( client.databaseContainer, client.apiClient @@ -242,7 +254,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP setMessagesObserver() } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { client.syncRepository.startTrackingChannelController(self) synchronize(isInRecoveryMode: false, completion) } @@ -266,7 +278,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP members: Set = [], invites: Set = [], extraData: [String: RawJSON] = [:], - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { @@ -310,7 +322,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP invites: Set = [], extraData: [String: RawJSON] = [:], unsetProperties: [String] = [], - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { @@ -342,7 +354,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. /// - public func muteChannel(expiration: Int? = nil, completion: ((Error?) -> Void)? = nil) { + public func muteChannel(expiration: Int? = nil, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -362,7 +374,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. /// - public func unmuteChannel(completion: ((Error?) -> Void)? = nil) { + public func unmuteChannel(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -385,7 +397,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - scope: The scope of the archiving action. Default is archiving for the current user only. /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - public func archive(scope: ChannelArchivingScope = .me, completion: ((Error?) -> Void)? = nil) { + public func archive(scope: ChannelArchivingScope = .me, completion: (@Sendable(Error?) -> Void)? = nil) { guard let cid, isChannelAlreadyCreated, let userId = client.currentUserId else { channelModificationFailed(completion) return @@ -406,7 +418,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - scope: The scope of the unarchiving action. The default scope is unarchived only for me. /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - public func unarchive(scope: ChannelArchivingScope = .me, completion: ((Error?) -> Void)? = nil) { + public func unarchive(scope: ChannelArchivingScope = .me, completion: (@Sendable(Error?) -> Void)? = nil) { guard let cid, isChannelAlreadyCreated, let userId = client.currentUserId else { channelModificationFailed(completion) return @@ -425,7 +437,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameters: /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - public func deleteChannel(completion: ((Error?) -> Void)? = nil) { + public func deleteChannel(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -454,7 +466,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP skipPush: Bool = false, hardDelete: Bool = true, systemMessage: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { @@ -481,7 +493,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. /// - public func hideChannel(clearHistory: Bool = false, completion: ((Error?) -> Void)? = nil) { + public func hideChannel(clearHistory: Bool = false, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -500,7 +512,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. /// - public func showChannel(completion: ((Error?) -> Void)? = nil) { + public func showChannel(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -525,7 +537,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP public func loadPreviousMessages( before messageId: MessageId? = nil, limit: Int? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard cid != nil, isChannelAlreadyCreated else { @@ -576,7 +588,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP public func loadNextMessages( after messageId: MessageId? = nil, limit: Int? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard cid != nil, isChannelAlreadyCreated else { @@ -623,7 +635,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - messageId: The message id of the message to jump to. /// - limit: The number of messages to load in total, including the message to jump to. /// - completion: Callback when the API call is completed. - public func loadPageAroundMessageId(_ messageId: MessageId, limit: Int? = nil, completion: ((Error?) -> Void)? = nil) { + public func loadPageAroundMessageId(_ messageId: MessageId, limit: Int? = nil, completion: (@Sendable(Error?) -> Void)? = nil) { guard isChannelAlreadyCreated else { channelModificationFailed(completion) return @@ -653,7 +665,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// Cleans the current state and loads the first page again. /// - Parameter completion: Callback when the API call is completed. - public func loadFirstPage(_ completion: ((_ error: Error?) -> Void)? = nil) { + public func loadFirstPage(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { channelQuery.pagination = .init( pageSize: channelQuery.pagination?.pageSize ?? .messagesPageSize, parameter: nil @@ -668,7 +680,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// /// - Parameter completion: a completion block with an error if the request was failed. /// - public func sendKeystrokeEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) { + public func sendKeystrokeEvent(parentMessageId: MessageId? = nil, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { completion?($0) } @@ -694,7 +706,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// /// - Parameter completion: a completion block with an error if the request was failed. /// - public func sendStartTypingEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) { + public func sendStartTypingEvent(parentMessageId: MessageId? = nil, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { completion?($0) } @@ -720,7 +732,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// /// - Parameter completion: a completion block with an error if the request was failed. /// - public func sendStopTypingEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) { + public func sendStopTypingEvent(parentMessageId: MessageId? = nil, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { completion?($0) } @@ -766,7 +778,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP skipEnrichUrl: Bool = false, restrictedVisibility: [UserId] = [], extraData: [String: RawJSON] = [:], - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { var transformableInfo = NewMessageTransformableInfo( text: text, @@ -807,7 +819,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP messageId: MessageId? = nil, restrictedVisibility: [UserId] = [], extraData: [String: RawJSON] = [:], - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { error in @@ -864,7 +876,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP quotedMessageId: MessageId? = nil, command: Command? = nil, extraData: [String: RawJSON] = [:], - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { error in @@ -896,7 +908,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// /// It is not necessary to call this method if the channel list query was called before. public func loadDraftMessage( - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { error in @@ -913,7 +925,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP } /// Deletes the draft message of this channel. - public func deleteDraftMessage(completion: ((Error?) -> Void)? = nil) { + public func deleteDraftMessage(completion: (@Sendable(Error?) -> Void)? = nil) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { error in completion?(error) @@ -951,7 +963,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP votingVisibility: VotingVisibility? = nil, options: [PollOption]? = nil, extraData: [String: RawJSON]? = nil, - completion: ((Result) -> Void)? + completion: (@Sendable(Result) -> Void)? ) { pollsRepository.createPoll( name: name, @@ -984,7 +996,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameters: /// - pollId: The id of the poll to be deleted. /// - completion: A closure to be executed once the poll is deleted, returning either an `Error` on failure or `nil` on success. - public func deletePoll(pollId: String, completion: ((Error?) -> Void)? = nil) { + public func deletePoll(pollId: String, completion: (@Sendable(Error?) -> Void)? = nil) { pollsRepository.deletePoll(pollId: pollId) { [weak self] error in self?.callback { completion?(error) @@ -1005,7 +1017,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP _ members: [MemberInfo], hideHistory: Bool = false, message: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { @@ -1038,7 +1050,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP userIds: Set, hideHistory: Bool = false, message: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { addMembers( userIds.map { .init(userId: $0, extraData: nil) }, @@ -1059,7 +1071,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP public func removeMembers( userIds: Set, message: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { @@ -1083,7 +1095,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameters: /// - userIds: Set of ids of users to be invited to the channel /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - public func inviteMembers(userIds: Set, completion: ((Error?) -> Void)? = nil) { + public func inviteMembers(userIds: Set, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1103,7 +1115,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - userId: userId /// - message: message /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - public func acceptInvite(message: String? = nil, completion: ((Error?) -> Void)? = nil) { + public func acceptInvite(message: String? = nil, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1120,7 +1132,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameters: /// - cid: The channel identifier. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - public func rejectInvite(completion: ((Error?) -> Void)? = nil) { + public func rejectInvite(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1139,7 +1151,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameter completion: The completion will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. /// - public func markRead(completion: ((Error?) -> Void)? = nil) { + public func markRead(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let channel = channel else { channelModificationFailed(completion) @@ -1164,7 +1176,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameters: /// - messageId: The id of the first message id that will be marked as unread. /// - completion: The completion will be called on a **callbackQueue** when the network request is finished. - public func markUnread(from messageId: MessageId, completion: ((Result) -> Void)? = nil) { + public func markUnread(from messageId: MessageId, completion: (@Sendable(Result) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let channel = channel else { let error = ClientError.ChannelNotCreatedYet() @@ -1204,7 +1216,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - completion: The completion to be called on **callbackQueue** when request is completed. public func loadChannelReads( pagination: Pagination? = nil, - completion: @escaping (Error?) -> Void + completion: @escaping @Sendable(Error?) -> Void ) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { completion($0 ?? ClientError.ChannelNotCreatedYet()) } @@ -1228,7 +1240,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameters: /// - limit: The number of channel reads to load. The default is 100. /// - completion: The completion to be called on **callbackQueue** when request is completed. - public func loadMoreChannelReads(limit: Int? = nil, completion: @escaping (Error?) -> Void) { + public func loadMoreChannelReads(limit: Int? = nil, completion: @escaping @Sendable(Error?) -> Void) { let pagination = Pagination(pageSize: limit ?? 100, offset: channel?.reads.count ?? 0) loadChannelReads(pagination: pagination, completion: completion) } @@ -1243,7 +1255,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - cooldownDuration: Duration of the time interval users have to wait between messages. /// Specified in seconds. Should be between 1-120. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - public func enableSlowMode(cooldownDuration: Int, completion: ((Error?) -> Void)? = nil) { + public func enableSlowMode(cooldownDuration: Int, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1268,7 +1280,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// /// - Parameters: /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - public func disableSlowMode(completion: ((Error?) -> Void)? = nil) { + public func disableSlowMode(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1291,7 +1303,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// /// /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - public func startWatching(isInRecoveryMode: Bool, completion: ((Error?) -> Void)? = nil) { + public func startWatching(isInRecoveryMode: Bool, completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1325,7 +1337,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// such as updating channel data. /// /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - public func stopWatching(completion: ((Error?) -> Void)? = nil) { + public func stopWatching(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1350,7 +1362,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. /// - public func freezeChannel(completion: ((Error?) -> Void)? = nil) { + public func freezeChannel(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1372,7 +1384,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. /// - public func unfreezeChannel(completion: ((Error?) -> Void)? = nil) { + public func unfreezeChannel(completion: (@Sendable(Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1395,7 +1407,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - scope: The scope of the pinning action. Default is pinning for the current user only. /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - public func pin(scope: ChannelPinningScope = .me, completion: ((Error?) -> Void)? = nil) { + public func pin(scope: ChannelPinningScope = .me, completion: (@Sendable(Error?) -> Void)? = nil) { guard let cid, isChannelAlreadyCreated, let userId = client.currentUserId else { channelModificationFailed(completion) return @@ -1416,7 +1428,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - scope: The scope of the unpinning action. The default scope is unpinned only for me. /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - public func unpin(scope: ChannelPinningScope = .me, completion: ((Error?) -> Void)? = nil) { + public func unpin(scope: ChannelPinningScope = .me, completion: (@Sendable(Error?) -> Void)? = nil) { guard let cid, isChannelAlreadyCreated, let userId = client.currentUserId else { channelModificationFailed(completion) return @@ -1440,8 +1452,8 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP public func uploadAttachment( localFileURL: URL, type: AttachmentType, - progress: ((Double) -> Void)? = nil, - completion: @escaping ((Result) -> Void) + progress: (@Sendable(Double) -> Void)? = nil, + completion: @escaping @Sendable(Result) -> Void ) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { error in @@ -1460,7 +1472,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// Get the link attachment preview data from the provided url. /// /// This will return the data present in the OG Metadata. - public func enrichUrl(_ url: URL, completion: @escaping (Result) -> Void) { + public func enrichUrl(_ url: URL, completion: @escaping @Sendable(Result) -> Void) { updater.enrichUrl(url) { result in self.callback { completion(result) @@ -1479,7 +1491,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP pageSize: Int = .messagesPageSize, sorting: [Sorting] = [], pagination: PinnedMessagesPagination? = nil, - completion: @escaping (Result<[ChatMessage], Error>) -> Void + completion: @escaping @Sendable(Result<[ChatMessage], Error>) -> Void ) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed { completion(.failure($0 ?? ClientError.ChannelNotCreatedYet())) } @@ -1524,7 +1536,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - url: The URL of the file to be deleted. /// - completion: An optional closure to be called when the delete operation is complete. /// If an error occurs during deletion, the error will be passed to this closure. - public func deleteFile(url: String, completion: ((Error?) -> Void)? = nil) { + public func deleteFile(url: String, completion: (@Sendable(Error?) -> Void)? = nil) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) return @@ -1538,7 +1550,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// - url: The URL of the image to be deleted. /// - completion: An optional closure to be called when the delete operation is complete. /// If an error occurs during deletion, the error will be passed to this closure. - public func deleteImage(url: String, completion: ((Error?) -> Void)? = nil) { + public func deleteImage(url: String, completion: (@Sendable(Error?) -> Void)? = nil) { guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) return @@ -1558,7 +1570,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP // MARK: - Internal - func recoverWatchedChannel(recovery: Bool, completion: @escaping (Error?) -> Void) { + func recoverWatchedChannel(recovery: Bool, completion: @escaping @Sendable(Error?) -> Void) { if cid != nil, isChannelAlreadyCreated { startWatching(isInRecoveryMode: recovery, completion: completion) } else { @@ -1579,7 +1591,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP restrictedVisibility: [UserId] = [], extraData: [String: RawJSON] = [:], poll: PollPayload?, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { @@ -1659,7 +1671,7 @@ extension ChatChannelController { } /// Describes the flow of the messages in the list -public enum MessageOrdering { +public enum MessageOrdering: Sendable { /// New messages appears on the top of the list. case topToBottom @@ -1670,7 +1682,7 @@ public enum MessageOrdering { // MARK: - Helpers private extension ChatChannelController { - func synchronize(isInRecoveryMode: Bool, _ completion: ((_ error: Error?) -> Void)? = nil) { + func synchronize(isInRecoveryMode: Bool, _ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { let channelCreatedCallback = isChannelAlreadyCreated ? nil : channelCreated(forwardErrorTo: setLocalStateBasedOnError) updater.update( channelQuery: channelQuery, @@ -1845,7 +1857,7 @@ private extension ChatChannelController { itemReuseKeyPaths: (\ChatMessage.id, \MessageDTO.id) ) observer.onDidChange = { [weak self] changes in - self?.delegateCallback { + self?.delegateCallback { [weak self] in guard let self = self else { return } log.debug("didUpdateMessages: \(changes.map(\.debugDescription))") @@ -1858,7 +1870,7 @@ private extension ChatChannelController { /// A convenience method that invokes the completion? with a ChannelFeatureDisabled error /// ie. VCs should use the `are{FEATURE_NAME}Enabled` props (ie. `areReadEventsEnabled`) before using any feature - private func channelFeatureDisabled(feature: String, completion: ((Error?) -> Void)?) { + private func channelFeatureDisabled(feature: String, completion: (@Sendable(Error?) -> Void)?) { let error = ClientError.ChannelFeatureDisabled("Channel feature: \(feature) is disabled for this channel.") log.error(error.localizedDescription) callback { @@ -1868,7 +1880,7 @@ private extension ChatChannelController { // It's impossible to perform any channel modification before it's creation on backend. // So before any modification attempt we need to check if channel is already created and call this function if not. - private func channelModificationFailed(_ completion: ((Error?) -> Void)?) { + private func channelModificationFailed(_ completion: (@Sendable(Error?) -> Void)?) { let error = ClientError.ChannelNotCreatedYet() log.error(error.localizedDescription) callback { @@ -1879,7 +1891,7 @@ private extension ChatChannelController { /// This callback is called after channel is created on backend but before channel is saved to DB. When channel is created /// we receive backend generated cid and setting up current `ChannelController` to observe this channel DB changes. /// Completion will be called if DB fetch will fail after setting new `ChannelQuery`. - private func channelCreated(forwardErrorTo completion: ((_ error: Error?) -> Void)?) -> ((ChannelId) -> Void) { + private func channelCreated(forwardErrorTo completion: (@Sendable(_ error: Error?) -> Void)?) -> (@Sendable(ChannelId) -> Void) { return { [weak self] cid in guard let self = self else { return } self.isChannelAlreadyCreated = true @@ -1888,7 +1900,7 @@ private extension ChatChannelController { } /// Helper for updating state after fetching local data. - private var setLocalStateBasedOnError: ((_ error: Error?) -> Void) { + private var setLocalStateBasedOnError: (@Sendable(_ error: Error?) -> Void) { return { [weak self] error in // Update observing state self?.state = error == nil ? .localDataFetched : .localDataFetchFailed(ClientError(with: error)) @@ -1917,25 +1929,25 @@ private extension ChatChannelController { // MARK: - Errors extension ClientError { - final class ChannelNotCreatedYet: ClientError { + final class ChannelNotCreatedYet: ClientError, @unchecked Sendable { override public var localizedDescription: String { "You can't modify the channel because the channel hasn't been created yet. Call `synchronize()` to create the channel and wait for the completion block to finish. Alternatively, you can observe the `state` changes of the controller and wait for the `remoteDataFetched` state." } } - final class ChannelEmptyMembers: ClientError { + final class ChannelEmptyMembers: ClientError, @unchecked Sendable { override public var localizedDescription: String { "You can't create direct messaging channel with empty members." } } - final class ChannelEmptyMessages: ClientError { + final class ChannelEmptyMessages: ClientError, @unchecked Sendable { override public var localizedDescription: String { "You can't load new messages when there is no messages in the channel." } } - final class InvalidCooldownDuration: ClientError { + final class InvalidCooldownDuration: ClientError, @unchecked Sendable { override public var localizedDescription: String { "You can't specify a value outside the range 1-120 for cooldown duration." } @@ -1943,7 +1955,7 @@ extension ClientError { } extension ClientError { - final class ChannelFeatureDisabled: ClientError {} + final class ChannelFeatureDisabled: ClientError, @unchecked Sendable {} } // MARK: - Deprecations @@ -1995,8 +2007,8 @@ public extension ChatChannelController { @available(*, deprecated, message: "use uploadAttachment() instead.") func uploadFile( localFileURL: URL, - progress: ((Double) -> Void)? = nil, - completion: @escaping ((Result) -> Void) + progress: (@Sendable(Double) -> Void)? = nil, + completion: @escaping @Sendable(Result) -> Void ) { uploadAttachment(localFileURL: localFileURL, type: .file, progress: progress) { result in completion(result.map(\.remoteURL)) @@ -2011,8 +2023,8 @@ public extension ChatChannelController { @available(*, deprecated, message: "use uploadAttachment() instead.") func uploadImage( localFileURL: URL, - progress: ((Double) -> Void)? = nil, - completion: @escaping ((Result) -> Void) + progress: (@Sendable(Double) -> Void)? = nil, + completion: @escaping @Sendable(Result) -> Void ) { uploadAttachment(localFileURL: localFileURL, type: .image, progress: progress) { result in completion(result.map(\.remoteURL)) diff --git a/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift b/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift index ca743167295..1d2a3131f54 100644 --- a/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift +++ b/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift @@ -24,7 +24,7 @@ extension ChatClient { /// - Returns: A new instance of `ChatChannelListController` public func channelListController( query: ChannelListQuery, - filter: ((ChatChannel) -> Bool)? = nil + filter: (@Sendable(ChatChannel) -> Bool)? = nil ) -> ChatChannelListController { .init(query: query, client: self, filter: filter) } @@ -33,7 +33,7 @@ extension ChatClient { /// `ChatChannelListController` is a controller class which allows observing a list of chat channels based on the provided query. /// /// - Note: For an async-await alternative of the `ChatChannelListController`, please check ``ChannelList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class ChatChannelListController: DataController, DelegateCallable, DataStoreProvider { +public class ChatChannelListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The query specifying and filtering the list of channels. public let query: ChannelListQuery @@ -51,14 +51,15 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt } /// The worker used to fetch the remote data and communicate with servers. - private lazy var worker: ChannelListUpdater = self.environment - .channelQueryUpdaterBuilder( - client.databaseContainer, - client.apiClient - ) + private let worker: ChannelListUpdater /// A Boolean value that returns whether pagination is finished - public private(set) var hasLoadedAllPreviousChannels: Bool = false + public private(set) var hasLoadedAllPreviousChannels: Bool { + get { queue.sync { _hasLoadedAllPreviousChannels } } + set { queue.sync { _hasLoadedAllPreviousChannels = newValue } } + } + + private var _hasLoadedAllPreviousChannels: Bool = false /// A type-erased delegate. var multicastDelegate: MulticastDelegate = .init() { @@ -105,7 +106,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt return observer }() - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -119,10 +120,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt private let filter: ((ChatChannel) -> Bool)? private let environment: Environment - private lazy var channelListLinker: ChannelListLinker = self.environment - .channelListLinkerBuilder( - query, filter, client.config, client.databaseContainer, worker - ) + private let channelListLinker: ChannelListLinker /// Creates a new `ChannelListController`. /// @@ -133,17 +131,24 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt init( query: ChannelListQuery, client: ChatClient, - filter: ((ChatChannel) -> Bool)? = nil, + filter: (@Sendable(ChatChannel) -> Bool)? = nil, environment: Environment = .init() ) { self.client = client self.query = query self.filter = filter self.environment = environment + worker = environment.channelQueryUpdaterBuilder( + client.databaseContainer, + client.apiClient + ) + channelListLinker = environment.channelListLinkerBuilder( + query, filter, client.config, client.databaseContainer, worker + ) super.init() } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startChannelListObserverIfNeeded() channelListLinker.start(with: client.eventNotificationCenter) client.syncRepository.startTrackingChannelListController(self) @@ -161,7 +166,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt /// public func loadNextChannels( limit: Int? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { if hasLoadedAllPreviousChannels { completion?(nil) @@ -183,7 +188,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt } @available(*, deprecated, message: "Please use `markAllRead` available in `CurrentChatUserController`") - public func markAllRead(completion: ((Error?) -> Void)? = nil) { + public func markAllRead(completion: (@Sendable(Error?) -> Void)? = nil) { worker.markAllRead { error in self.callback { completion?(error) @@ -193,7 +198,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt // MARK: - Internal - func refreshLoadedChannels(completion: @escaping (Result, Error>) -> Void) { + func refreshLoadedChannels(completion: @escaping @Sendable(Result, Error>) -> Void) { let channelCount = channelListObserver.items.count worker.refreshLoadedChannels(for: query, channelCount: channelCount, completion: completion) } @@ -201,7 +206,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt // MARK: - Helpers private func updateChannelList( - _ completion: ((_ error: Error?) -> Void)? = nil + _ completion: (@Sendable(_ error: Error?) -> Void)? = nil ) { let limit = query.pagination.pageSize worker.update( @@ -246,7 +251,7 @@ extension ChatChannelListController { var channelListLinkerBuilder: ( _ query: ChannelListQuery, - _ filter: ((ChatChannel) -> Bool)?, + _ filter: (@Sendable(ChatChannel) -> Bool)?, _ clientConfig: ChatClientConfig, _ databaseContainer: DatabaseContainer, _ worker: ChannelListUpdater diff --git a/Sources/StreamChat/Controllers/ChannelWatcherListController/ChatChannelWatcherListController.swift b/Sources/StreamChat/Controllers/ChannelWatcherListController/ChatChannelWatcherListController.swift index 6a631a71ceb..3abd92697fe 100644 --- a/Sources/StreamChat/Controllers/ChannelWatcherListController/ChatChannelWatcherListController.swift +++ b/Sources/StreamChat/Controllers/ChannelWatcherListController/ChatChannelWatcherListController.swift @@ -17,7 +17,7 @@ extension ChatClient { /// `ChatChannelWatcherListController` is a controller class which allows observing /// a list of chat watchers based on the provided query. /// -public class ChatChannelWatcherListController: DataController, DelegateCallable, DataStoreProvider { +public class ChatChannelWatcherListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The query specifying sorting and filtering for the list of channel watchers. @Atomic public private(set) var query: ChannelWatcherListQuery @@ -32,7 +32,7 @@ public class ChatChannelWatcherListController: DataController, DelegateCallable, return watchersObserver.items } - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -58,13 +58,7 @@ public class ChatChannelWatcherListController: DataController, DelegateCallable, private lazy var watchersObserver: BackgroundListDatabaseObserver = createWatchersObserver() /// The worker used to fetch the remote data and communicate with servers. - private lazy var updater: ChannelUpdater = self.environment.channelUpdaterBuilder( - client.channelRepository, - client.messageRepository, - client.makeMessagesPaginationStateHandler(), - client.databaseContainer, - client.apiClient - ) + private let updater: ChannelUpdater private let environment: Environment @@ -77,12 +71,19 @@ public class ChatChannelWatcherListController: DataController, DelegateCallable, self.client = client self.query = query self.environment = environment + updater = environment.channelUpdaterBuilder( + client.channelRepository, + client.messageRepository, + client.makeMessagesPaginationStateHandler(), + client.databaseContainer, + client.apiClient + ) } /// Synchronizes the channel's watchers with the backend. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startObservingIfNeeded() if case let .localDataFetchFailed(error) = state { @@ -174,10 +175,10 @@ public extension ChatChannelWatcherListController { /// - limit: Limit for page size. Offset is defined automatically by the controller. /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - func loadNextWatchers(limit: Int = .channelWatchersPageSize, completion: ((Error?) -> Void)? = nil) { + func loadNextWatchers(limit: Int = .channelWatchersPageSize, completion: (@Sendable(Error?) -> Void)? = nil) { var updatedQuery = query updatedQuery.pagination = .init(pageSize: limit, offset: watchers.count) - updater.channelWatchers(query: updatedQuery) { result in + updater.channelWatchers(query: updatedQuery) { [updatedQuery] result in self.query = updatedQuery self.callback { completion?(result.error) } } diff --git a/Sources/StreamChat/Controllers/ConnectionController/ConnectionController.swift b/Sources/StreamChat/Controllers/ConnectionController/ConnectionController.swift index d954faea8cb..56c3aec140d 100644 --- a/Sources/StreamChat/Controllers/ConnectionController/ConnectionController.swift +++ b/Sources/StreamChat/Controllers/ConnectionController/ConnectionController.swift @@ -17,10 +17,15 @@ public extension ChatClient { /// `ChatConnectionController` is a controller class which allows to explicitly /// connect/disconnect the `ChatClient` and observe connection events. -public class ChatConnectionController: Controller, DelegateCallable, DataStoreProvider { - public var callbackQueue: DispatchQueue = .main - - var _basePublishers: Any? +public class ChatConnectionController: Controller, DelegateCallable, DataStoreProvider, @unchecked Sendable { + public var callbackQueue: DispatchQueue { + get { queue.sync { _callbackQueue } } + set { queue.sync { _callbackQueue = newValue } } + } + + private let queue = DispatchQueue(label: "io.getstream.chat-connection-controller", target: .global(qos: .userInteractive)) + private var _callbackQueue: DispatchQueue = .main + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -42,7 +47,7 @@ public class ChatConnectionController: Controller, DelegateCallable, DataStorePr } /// The connection event observer for the connection status updates. - private var connectionEventObserver: ConnectionEventObserver? + private var _connectionEventObserver: ConnectionEventObserver? /// A type-erased delegate. var multicastDelegate: MulticastDelegate = .init() @@ -63,7 +68,7 @@ public class ChatConnectionController: Controller, DelegateCallable, DataStorePr self.connectionRepository = connectionRepository self.webSocketClient = webSocketClient self.client = client - connectionEventObserver = setupObserver() + _connectionEventObserver = setupObserver() } private func setupObserver() -> ConnectionEventObserver? { @@ -91,7 +96,7 @@ public extension ChatConnectionController { /// - Parameter completion: Called when the connection is established. If the connection fails, the completion is /// called with an error. /// - func connect(completion: ((Error?) -> Void)? = nil) { + func connect(completion: (@Sendable(Error?) -> Void)? = nil) { connectionRepository.connect { [weak self] error in self?.callback { completion?(error) @@ -132,8 +137,8 @@ public extension ChatConnectionController { private class ConnectionEventObserver: EventObserver { init( notificationCenter: NotificationCenter, - filter: ((ConnectionStatusUpdated) -> Bool)? = nil, - callback: @escaping (ConnectionStatusUpdated) -> Void + filter: (@Sendable(ConnectionStatusUpdated) -> Bool)? = nil, + callback: @escaping @Sendable(ConnectionStatusUpdated) -> Void ) { super.init(notificationCenter: notificationCenter, transform: { $0 as? ConnectionStatusUpdated }) { guard filter == nil || filter?($0) == true else { return } diff --git a/Sources/StreamChat/Controllers/Controller.swift b/Sources/StreamChat/Controllers/Controller.swift index 062b694c20d..79c95ea0cd2 100644 --- a/Sources/StreamChat/Controllers/Controller.swift +++ b/Sources/StreamChat/Controllers/Controller.swift @@ -15,7 +15,7 @@ public protocol Controller { extension Controller { /// A helper function to ensure the callback is performed on the callback queue. - func callback(_ action: @escaping () -> Void) { + func callback(_ action: @escaping @Sendable() -> Void) { // We perform the callback synchronously if we're on main & `callbackQueue` is on main, too. if Thread.current.isMainThread && callbackQueue == .main { action() diff --git a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift index 83ec7c2b45c..40385a8a074 100644 --- a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift +++ b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift @@ -19,13 +19,13 @@ public extension ChatClient { /// user of `ChatClient`. /// /// - Note: For an async-await alternative of the `CurrentChatUserController`, please check ``ConnectedUser`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class CurrentChatUserController: DataController, DelegateCallable, DataStoreProvider { +public class CurrentChatUserController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The `ChatClient` instance this controller belongs to. public let client: ChatClient - + private let environment: Environment - - var _basePublishers: Any? + + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -36,7 +36,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt _basePublishers = BasePublishers(controller: self) return _basePublishers as? BasePublishers ?? .init(controller: self) } - + /// Used for observing the current user changes in a database. private lazy var currentUserObserver = createUserObserver() .onChange { [weak self] change in @@ -57,7 +57,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt $0.currentUserController(self, didChangeCurrentUserUnreadCount: change.unreadCount) } } - + /// A type-erased delegate. var multicastDelegate: MulticastDelegate = .init() @@ -68,37 +68,54 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt startObservingIfNeeded() return currentUserObserver.item } - + /// The unread messages and channels count for the current user. /// /// Returns `noUnread` if `currentUser` doesn't exist yet. public var unreadCount: UnreadCount { currentUser?.unreadCount ?? .noUnread } - + /// The worker used to update the current user. - private lazy var currentUserUpdater = environment.currentUserUpdaterBuilder( - client.databaseContainer, - client.apiClient - ) - + private let currentUserUpdater: CurrentUserUpdater + /// The worker used to update the current user member for a given channel. - private lazy var currentMemberUpdater = createMemberUpdater() - + private let currentMemberUpdater: ChannelMemberUpdater + /// The query used for fetching the draft messages. - private var draftListQuery = DraftListQuery() + private var draftListQuery: DraftListQuery { + get { queue.sync { _draftListQuery } } + set { queue.sync { _draftListQuery = newValue } } + } + private var _draftListQuery = DraftListQuery() + /// Use for observing the current user's draft messages changes. - private var draftMessagesObserver: BackgroundListDatabaseObserver? + private var draftMessagesObserver: BackgroundListDatabaseObserver? { + get { queue.sync { _draftMessagesObserver } } + set { queue.sync { _draftMessagesObserver = newValue } } + } + private var _draftMessagesObserver: BackgroundListDatabaseObserver? + /// The repository for draft messages. - private var draftMessagesRepository: DraftMessagesRepository - + private let draftMessagesRepository: DraftMessagesRepository + /// The token for the next page of draft messages. - private var draftMessagesNextCursor: String? + private var draftMessagesNextCursor: String? { + get { queue.sync { _draftMessagesNextCursor } } + set { queue.sync { _draftMessagesNextCursor = newValue } } + } + private var _draftMessagesNextCursor: String? + /// A flag to indicate whether all draft messages have been loaded. - public private(set) var hasLoadedAllDrafts: Bool = false + public private(set) var hasLoadedAllDrafts: Bool { + get { queue.sync { _hasLoadedAllDrafts } } + set { queue.sync { _hasLoadedAllDrafts = newValue } } + } + + private var _hasLoadedAllDrafts: Bool = false /// The current user's draft messages. public var draftMessages: [DraftMessage] { @@ -119,6 +136,14 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt init(client: ChatClient, environment: Environment = .init()) { self.client = client self.environment = environment + currentMemberUpdater = ChannelMemberUpdater( + database: client.databaseContainer, + apiClient: client.apiClient + ) + currentUserUpdater = environment.currentUserUpdaterBuilder( + client.databaseContainer, + client.apiClient + ) draftMessagesRepository = client.draftMessagesRepository } @@ -127,7 +152,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt /// /// - Parameter completion: Called when the controller has finished fetching the local data /// and the client connection is established. - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startObservingIfNeeded() if case let .localDataFetchFailed(error) = state { @@ -145,7 +170,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt } self?.state = error == nil ? .remoteDataFetched : .remoteDataFetchFailed(error!) - self?.callback { completion?(error) } + self?.callback { [error] in completion?(error) } } } @@ -173,7 +198,7 @@ public extension CurrentChatUserController { /// database will be flushed. /// /// - Parameter completion: The completion to be called when the operation is completed. - func reloadUserIfNeeded(completion: ((Error?) -> Void)? = nil) { + func reloadUserIfNeeded(completion: (@Sendable(Error?) -> Void)? = nil) { client.authenticationRepository.refreshToken { error in self.callback { completion?(error) @@ -204,7 +229,7 @@ public extension CurrentChatUserController { teamsRole: [TeamId: UserRole]? = nil, userExtraData: [String: RawJSON] = [:], unsetProperties: Set = [], - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { guard let currentUserId = client.currentUserId else { completion?(ClientError.CurrentUserDoesNotExist()) @@ -240,7 +265,7 @@ public extension CurrentChatUserController { _ extraData: [String: RawJSON], unsetProperties: [String]? = nil, in channelId: ChannelId, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { guard let currentUserId = client.currentUserId else { completion?(.failure(ClientError.CurrentUserDoesNotExist())) @@ -261,7 +286,7 @@ public extension CurrentChatUserController { /// Fetches the most updated devices and syncs with the local database. /// - Parameter completion: Called when the devices are synced successfully, or with error. - func synchronizeDevices(completion: ((Error?) -> Void)? = nil) { + func synchronizeDevices(completion: (@Sendable(Error?) -> Void)? = nil) { guard let currentUserId = client.currentUserId else { completion?(ClientError.CurrentUserDoesNotExist()) return @@ -276,7 +301,7 @@ public extension CurrentChatUserController { /// - Parameters: /// - pushDevice: The device information required for the desired push provider. /// - completion: Callback when device is successfully registered, or failed with error. - func addDevice(_ pushDevice: PushDevice, completion: ((Error?) -> Void)? = nil) { + func addDevice(_ pushDevice: PushDevice, completion: (@Sendable(Error?) -> Void)? = nil) { guard let currentUserId = client.currentUserId else { completion?(ClientError.CurrentUserDoesNotExist()) return @@ -300,7 +325,7 @@ public extension CurrentChatUserController { /// - id: Device id to be removed. You can obtain registered devices via `currentUser.devices`. /// If `currentUser.devices` is not up-to-date, please make an `synchronize` call. /// - completion: Called when device is successfully deregistered, or with error. - func removeDevice(id: DeviceId, completion: ((Error?) -> Void)? = nil) { + func removeDevice(id: DeviceId, completion: (@Sendable(Error?) -> Void)? = nil) { guard let currentUserId = client.currentUserId else { completion?(ClientError.CurrentUserDoesNotExist()) return @@ -317,7 +342,7 @@ public extension CurrentChatUserController { /// /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func markAllRead(completion: ((Error?) -> Void)? = nil) { + func markAllRead(completion: (@Sendable(Error?) -> Void)? = nil) { currentUserUpdater.markAllRead { error in self.callback { completion?(error) @@ -328,7 +353,7 @@ public extension CurrentChatUserController { /// Deletes all the local downloads of file attachments. /// /// - Parameter completion: Called when files have been deleted or when an error occured. - func deleteAllLocalAttachmentDownloads(completion: ((Error?) -> Void)? = nil) { + func deleteAllLocalAttachmentDownloads(completion: (@Sendable(Error?) -> Void)? = nil) { currentUserUpdater.deleteAllLocalAttachmentDownloads { error in guard let completion else { return } self.callback { @@ -343,7 +368,7 @@ public extension CurrentChatUserController { /// Returns the current user unreads or an error if the API call fails. /// /// Note: This is a one-time request, it is not observable. - func loadAllUnreads(completion: @escaping ((Result) -> Void)) { + func loadAllUnreads(completion: @escaping (@Sendable(Result) -> Void)) { currentUserUpdater.loadAllUnreads { result in self.callback { completion(result) @@ -355,7 +380,7 @@ public extension CurrentChatUserController { /// /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func loadBlockedUsers(completion: @escaping (Result<[BlockedUserDetails], Error>) -> Void) { + func loadBlockedUsers(completion: @escaping @Sendable(Result<[BlockedUserDetails], Error>) -> Void) { currentUserUpdater.loadBlockedUsers { result in self.callback { completion(result) @@ -374,7 +399,7 @@ public extension CurrentChatUserController { /// It is optional since it can be observed from the delegate events. func loadDraftMessages( query: DraftListQuery = DraftListQuery(), - completion: ((Result<[DraftMessage], Error>) -> Void)? = nil + completion: (@Sendable(Result<[DraftMessage], Error>) -> Void)? = nil ) { draftListQuery = query createDraftMessagesObserver(query: query) @@ -400,7 +425,7 @@ public extension CurrentChatUserController { /// It is optional since it can be observed from the delegate events. func loadMoreDraftMessages( limit: Int? = nil, - completion: ((Result<[DraftMessage], Error>) -> Void)? = nil + completion: (@Sendable(Result<[DraftMessage], Error>) -> Void)? = nil ) { guard let nextCursor = draftMessagesNextCursor else { completion?(.success([])) @@ -429,7 +454,7 @@ public extension CurrentChatUserController { func deleteDraftMessage( for cid: ChannelId, threadId: MessageId? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { draftMessagesRepository.deleteDraft(for: cid, threadId: threadId) { error in self.callback { @@ -487,10 +512,6 @@ private extension CurrentChatUserController { ) } - private func createMemberUpdater() -> ChannelMemberUpdater { - .init(database: client.databaseContainer, apiClient: client.apiClient) - } - @discardableResult private func createDraftMessagesObserver(query: DraftListQuery) -> BackgroundListDatabaseObserver { let observer = environment.draftMessagesObserverBuilder( @@ -554,7 +575,7 @@ public extension CurrentChatUserController { deprecated, message: "use addDevice(_pushDevice:) instead. This deprecated function doesn't correctly support multiple push providers." ) - func addDevice(token: Data, pushProvider: PushProvider = .apn, completion: ((Error?) -> Void)? = nil) { + func addDevice(token: Data, pushProvider: PushProvider = .apn, completion: (@Sendable(Error?) -> Void)? = nil) { addDevice(.apn(token: token), completion: completion) } } diff --git a/Sources/StreamChat/Controllers/DataController.swift b/Sources/StreamChat/Controllers/DataController.swift index 2511e6286be..55045a2a95a 100644 --- a/Sources/StreamChat/Controllers/DataController.swift +++ b/Sources/StreamChat/Controllers/DataController.swift @@ -5,9 +5,9 @@ import Foundation /// The base class for controllers which represent and control a data entity. Not meant to be used directly. -public class DataController: Controller { +public class DataController: Controller, @unchecked Sendable { /// Describes the possible states of `DataController` - public enum State: Equatable { + public enum State: Equatable, Sendable { /// The controller is created but no data fetched. case initialized /// The controllers already fetched local data if any. @@ -20,14 +20,22 @@ public class DataController: Controller { case remoteDataFetchFailed(ClientError) } + let queue = DispatchQueue(label: "io.getstream.data-controller", target: .global(qos: .userInteractive)) + /// The current state of the controller. - public internal(set) var state: State = .initialized { - didSet { + public internal(set) var state: State { + get { + queue.sync { _state } + } + set { + queue.sync { _state = newValue } callback { self.stateMulticastDelegate.invoke { $0.controller(self, didChangeState: self.state) } } } } + + private var _state: State = .initialized /// Determines whether the controller can be recovered. A failure fetching remote data can mean that we failed to fetch the data that is present on the server, or /// that we failed to synchronize a locally created channel @@ -50,7 +58,7 @@ public class DataController: Controller { /// the `error` variable contains more details about the problem. /// // swiftlint:disable unavailable_function - public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { fatalError("`synchronize` method must be overriden by the subclass.") } @@ -80,9 +88,9 @@ public extension DataControllerStateDelegate { } /// A helper protocol allowing calling delegate using existing `callback` method. -protocol DelegateCallable { +protocol DelegateCallable: Sendable { associatedtype Delegate - func callback(_ action: @escaping () -> Void) + func callback(_ action: @escaping @Sendable() -> Void) /// The multicast delegate wrapper for all delegates of the controller var multicastDelegate: MulticastDelegate { get } @@ -90,7 +98,7 @@ protocol DelegateCallable { extension DelegateCallable { /// A helper function to ensure the delegate callback is performed using the `callback` method. - func delegateCallback(_ callback: @escaping (Delegate) -> Void) { + func delegateCallback(_ callback: @escaping @Sendable(Delegate) -> Void) { self.callback { self.multicastDelegate.invoke(callback) } diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundDatabaseObserver.swift b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundDatabaseObserver.swift index 4e795a5c04d..ed26b65400e 100644 --- a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundDatabaseObserver.swift +++ b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundDatabaseObserver.swift @@ -5,14 +5,14 @@ import CoreData import Foundation -class BackgroundDatabaseObserver { +class BackgroundDatabaseObserver: @unchecked Sendable { /// Called with the aggregated changes after the internal `NSFetchResultsController` calls `controllerWillChangeContent` /// on its delegate. - var onWillChange: (() -> Void)? + var onWillChange: (@Sendable() -> Void)? /// Called with the aggregated changes after the internal `NSFetchResultsController` calls `controllerDidChangeContent` /// on its delegate. - var onDidChange: (([ListChange]) -> Void)? + var onDidChange: (@Sendable([ListChange]) -> Void)? /// Used to convert the `DTO`s to the resulting `Item`s. private let itemCreator: (DTO) throws -> Item diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swift b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swift index 4dc8b5be191..3920c7efbf7 100644 --- a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swift +++ b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swift @@ -7,7 +7,7 @@ import Foundation /// Observes changes of a single entity specified using an `NSFetchRequest`in the provided `NSManagedObjectContext`. /// This observation is performed on the background -class BackgroundEntityDatabaseObserver: BackgroundDatabaseObserver { +class BackgroundEntityDatabaseObserver: BackgroundDatabaseObserver, @unchecked Sendable { var item: Item? { rawItems.first } diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundListDatabaseObserver.swift b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundListDatabaseObserver.swift index cf1b18803b6..00f03661463 100644 --- a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundListDatabaseObserver.swift +++ b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundListDatabaseObserver.swift @@ -5,7 +5,7 @@ import CoreData import Foundation -class BackgroundListDatabaseObserver: BackgroundDatabaseObserver { +class BackgroundListDatabaseObserver: BackgroundDatabaseObserver, @unchecked Sendable { var items: LazyCachedMapCollection { LazyCachedMapCollection(elements: rawItems) } diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/EntityChange.swift b/Sources/StreamChat/Controllers/DatabaseObserver/EntityChange.swift index 4bbcad34ce0..0d9f902b7f3 100644 --- a/Sources/StreamChat/Controllers/DatabaseObserver/EntityChange.swift +++ b/Sources/StreamChat/Controllers/DatabaseObserver/EntityChange.swift @@ -58,6 +58,7 @@ extension EntityChange { } extension EntityChange: Equatable where Item: Equatable {} +extension EntityChange: Sendable where Item: Sendable {} extension EntityChange { /// Create a `EntityChange` value from the provided `ListChange`. It simply transforms `ListChange` to `EntityChange` diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift b/Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift index d0007a0cdd4..93d4de21114 100644 --- a/Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift +++ b/Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift @@ -136,6 +136,7 @@ extension ListChange { } extension ListChange: Equatable where Item: Equatable {} +extension ListChange: Sendable where Item: Sendable {} /// When this object is set as `NSFetchedResultsControllerDelegate`, it aggregates the callbacks from the fetched results /// controller and forwards them in the way of `[Change]`. You can set the `onDidChange` callback to receive these updates. diff --git a/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift b/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift index 3305983cb47..155e7e801f7 100644 --- a/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift +++ b/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift @@ -38,7 +38,7 @@ public extension ChatChannelController { /// `ChannelEventsController` is a controller class which allows to observe channel /// events and send custom events. -public class ChannelEventsController: EventsController { +public class ChannelEventsController: EventsController, @unchecked Sendable { // A channel identifier provider. private let cidProvider: () -> ChannelId? @@ -69,7 +69,7 @@ public class ChannelEventsController: EventsController { /// - Parameters: /// - payload: A custom event payload to be sent. /// - completion: A completion. - public func sendEvent(_ payload: T, completion: ((Error?) -> Void)? = nil) { + public func sendEvent(_ payload: T, completion: (@Sendable(Error?) -> Void)? = nil) { guard let cid = cid else { callback { completion?(ClientError.ChannelNotCreatedYet()) } return diff --git a/Sources/StreamChat/Controllers/EventsController/EventsController.swift b/Sources/StreamChat/Controllers/EventsController/EventsController.swift index f0c50edc9d3..cee9691df70 100644 --- a/Sources/StreamChat/Controllers/EventsController/EventsController.swift +++ b/Sources/StreamChat/Controllers/EventsController/EventsController.swift @@ -23,14 +23,14 @@ public protocol EventsControllerDelegate: AnyObject { } /// `EventsController` is a controller class which allows to observe custom and system events. -public class EventsController: Controller, DelegateCallable { +public class EventsController: Controller, DelegateCallable, @unchecked Sendable { // An underlaying observer listening for events. private var observer: EventObserver! /// A callback queue on which delegate methods are invoked. public var callbackQueue: DispatchQueue = .main - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -50,6 +50,8 @@ public class EventsController: Controller, DelegateCallable { get { multicastDelegate.mainDelegate } set { multicastDelegate.set(mainDelegate: newValue) } } + + private let queue = DispatchQueue(label: "io.getstream.events-controller", target: .global()) /// Create a new instance of `EventsController`. /// diff --git a/Sources/StreamChat/Controllers/MemberController/MemberController.swift b/Sources/StreamChat/Controllers/MemberController/MemberController.swift index 3f5039af1e3..1d0ae2c94a7 100644 --- a/Sources/StreamChat/Controllers/MemberController/MemberController.swift +++ b/Sources/StreamChat/Controllers/MemberController/MemberController.swift @@ -18,7 +18,7 @@ public extension ChatClient { /// `ChatChannelMemberController` is a controller class which allows mutating and observing changes of a specific chat member. /// -public class ChatChannelMemberController: DataController, DelegateCallable, DataStoreProvider { +public class ChatChannelMemberController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The identifier of the user this controller observes. public let userId: UserId @@ -37,7 +37,7 @@ public class ChatChannelMemberController: DataController, DelegateCallable, Data return memberObserver.item } - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -59,10 +59,10 @@ public class ChatChannelMemberController: DataController, DelegateCallable, Data } /// The worker used to update channel members. - private lazy var memberUpdater = createMemberUpdater() + private let memberUpdater: ChannelMemberUpdater /// The worker used to fetch channel members. - private lazy var memberListUpdater = createMemberListUpdater() + private let memberListUpdater: ChannelMemberListUpdater /// The observer used to track the user changes in the database. private lazy var memberObserver = createMemberObserver() @@ -95,9 +95,17 @@ public class ChatChannelMemberController: DataController, DelegateCallable, Data self.cid = cid self.client = client self.environment = environment + memberUpdater = environment.memberUpdaterBuilder( + client.databaseContainer, + client.apiClient + ) + memberListUpdater = environment.memberListUpdaterBuilder( + client.databaseContainer, + client.apiClient + ) } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startObservingIfNeeded() if case let .localDataFetchFailed(error) = state { @@ -113,20 +121,6 @@ public class ChatChannelMemberController: DataController, DelegateCallable, Data // MARK: - Private - private func createMemberUpdater() -> ChannelMemberUpdater { - environment.memberUpdaterBuilder( - client.databaseContainer, - client.apiClient - ) - } - - private func createMemberListUpdater() -> ChannelMemberListUpdater { - environment.memberListUpdaterBuilder( - client.databaseContainer, - client.apiClient - ) - } - private func createMemberObserver() -> BackgroundEntityDatabaseObserver { environment.memberObserverBuilder( client.databaseContainer, @@ -162,7 +156,7 @@ public extension ChatChannelMemberController { func partialUpdate( extraData: [String: RawJSON]?, unsetProperties: [String]? = nil, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { memberUpdater.partialUpdate( userId: userId, @@ -186,7 +180,7 @@ public extension ChatChannelMemberController { func ban( for timeoutInMinutes: Int? = nil, reason: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { memberUpdater.banMember( userId, @@ -211,7 +205,7 @@ public extension ChatChannelMemberController { func shadowBan( for timeoutInMinutes: Int? = nil, reason: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { memberUpdater.banMember( userId, @@ -229,7 +223,7 @@ public extension ChatChannelMemberController { /// Unbans the channel member. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - func unban(completion: ((Error?) -> Void)? = nil) { + func unban(completion: (@Sendable(Error?) -> Void)? = nil) { memberUpdater.unbanMember(userId, in: cid) { error in self.callback { completion?(error) diff --git a/Sources/StreamChat/Controllers/MemberListController/MemberListController.swift b/Sources/StreamChat/Controllers/MemberListController/MemberListController.swift index 244fb03bc4d..15e288ffa0a 100644 --- a/Sources/StreamChat/Controllers/MemberListController/MemberListController.swift +++ b/Sources/StreamChat/Controllers/MemberListController/MemberListController.swift @@ -19,7 +19,7 @@ extension ChatClient { /// `ChatChannelMemberListController` is a controller class which allows observing /// a list of chat users based on the provided query. /// - Note: For an async-await alternative of the `ChatChannelMemberListControler`, please check ``MemberList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class ChatChannelMemberListController: DataController, DelegateCallable, DataStoreProvider { +public class ChatChannelMemberListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The query specifying sorting and filtering for the list of channel members. @Atomic public private(set) var query: ChannelMemberListQuery @@ -35,7 +35,7 @@ public class ChatChannelMemberListController: DataController, DelegateCallable, } /// The worker used to fetch the remote data and communicate with servers. - private lazy var memberListUpdater = createMemberListUpdater() + private let memberListUpdater: ChannelMemberListUpdater /// The observer used to observe the changes in the database. private lazy var memberListObserver = createMemberListObserver() @@ -50,7 +50,7 @@ public class ChatChannelMemberListController: DataController, DelegateCallable, } } - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -73,9 +73,13 @@ public class ChatChannelMemberListController: DataController, DelegateCallable, self.client = client self.query = query self.environment = environment + memberListUpdater = environment.memberListUpdaterBuilder( + client.databaseContainer, + client.apiClient + ) } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startObservingIfNeeded() if case let .localDataFetchFailed(error) = state { @@ -89,13 +93,6 @@ public class ChatChannelMemberListController: DataController, DelegateCallable, } } - private func createMemberListUpdater() -> ChannelMemberListUpdater { - environment.memberListUpdaterBuilder( - client.databaseContainer, - client.apiClient - ) - } - private func createMemberListObserver() -> BackgroundListDatabaseObserver { let observer = environment.memberListObserverBuilder( client.databaseContainer, @@ -139,12 +136,13 @@ public extension ChatChannelMemberListController { /// If request fails, the completion will be called with an error. func loadNextMembers( limit: Int = 25, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { - var updatedQuery = query - updatedQuery.pagination = Pagination(pageSize: limit, offset: members.count) - memberListUpdater.load(updatedQuery) { result in - self.query = updatedQuery + let pagination = Pagination(pageSize: limit, offset: members.count) + let updatedQuery = query.withPagination(pagination) + memberListUpdater.load(updatedQuery) { [weak self] result in + guard let self else { return } + self.query = self.query.withPagination(pagination) self.callback { completion?(result.error) } diff --git a/Sources/StreamChat/Controllers/MessageController/MessageController.swift b/Sources/StreamChat/Controllers/MessageController/MessageController.swift index de88b4c0f67..205bd0be39e 100644 --- a/Sources/StreamChat/Controllers/MessageController/MessageController.swift +++ b/Sources/StreamChat/Controllers/MessageController/MessageController.swift @@ -18,7 +18,7 @@ public extension ChatClient { /// `ChatMessageController` is a controller class which allows observing and mutating a chat message entity. /// /// - Note: For an async-await alternative of the `ChatMessageController`, please check ``Chat`` and ``MessageState`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class ChatMessageController: DataController, DelegateCallable, DataStoreProvider { +public class ChatMessageController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The `ChatClient` instance this controller belongs to. public let client: ChatClient @@ -29,7 +29,12 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP public let messageId: MessageId /// The amount of replies fetched per page. - public var repliesPageSize: Int = .messagesPageSize + public var repliesPageSize: Int { + get { queue.sync { _repliesPageSize } } + set { queue.sync { _repliesPageSize = newValue } } + } + + private var _repliesPageSize: Int = .messagesPageSize /// The message object this controller represents. /// @@ -56,8 +61,10 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// To observe changes of the reactions, set your class as a delegate of this controller or use the provided /// `Combine` publishers. /// - public var reactions: [ChatMessageReaction] = [] { - didSet { + public var reactions: [ChatMessageReaction] { + get { queue.sync { _reactions } } + set { + queue.sync { _reactions = newValue } delegateCallback { [weak self] in guard let self = self else { log.warning("Callback called while self is nil") @@ -69,8 +76,15 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP } } + private var _reactions: [ChatMessageReaction] = [] + /// A Boolean value that returns whether the reactions have all been loaded or not. - public internal(set) var hasLoadedAllReactions = false + public internal(set) var hasLoadedAllReactions: Bool { + get { queue.sync { _hasLoadedAllReactions } } + set { queue.sync { _hasLoadedAllReactions = newValue } } + } + + private var _hasLoadedAllReactions = false /// Describes the ordering the replies are presented. /// @@ -78,8 +92,10 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// the `listOrdering` value to reflect the changes. Further updates to the replies will be delivered using the delegate /// methods, as usual. /// - public var listOrdering: MessageOrdering = .topToBottom { - didSet { + public var listOrdering: MessageOrdering { + get { queue.sync { _listOrdering } } + set { + queue.sync { _listOrdering = newValue } if state != .initialized { setRepliesObserver() @@ -98,6 +114,8 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP } } + private var _listOrdering: MessageOrdering = .topToBottom + /// A Boolean value that returns whether the oldest replies have all been loaded or not. public var hasLoadedAllPreviousReplies: Bool { replyPaginationState.hasLoadedAllPreviousMessages @@ -142,7 +160,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP private let environment: Environment - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -215,7 +233,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP setRepliesObserver() } - override public func synchronize(_ completion: ((Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(Error?) -> Void)? = nil) { startObserversIfNeeded() messageUpdater.getMessage(cid: cid, messageId: messageId) { result in @@ -261,7 +279,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP skipEnrichUrl: Bool = false, attachments: [AnyAttachmentPayload] = [], extraData: [String: RawJSON]? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { var transformableInfo = NewMessageTransformableInfo( text: text, @@ -294,7 +312,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. /// - public func deleteMessage(hard: Bool = false, completion: ((Error?) -> Void)? = nil) { + public func deleteMessage(hard: Bool = false, completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.deleteMessage(messageId: messageId, hard: hard) { error in self.callback { completion?(error) @@ -333,7 +351,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP skipPush: Bool = false, skipEnrichUrl: Bool = false, extraData: [String: RawJSON] = [:], - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { let parentMessageId = self.messageId @@ -384,7 +402,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP public func loadPreviousReplies( before replyId: MessageId? = nil, limit: Int? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { if hasLoadedAllPreviousReplies || isLoadingPreviousReplies { completion?(nil) @@ -445,7 +463,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP public func loadPageAroundReplyId( _ replyId: MessageId, limit: Int? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { if isLoadingMiddleReplies { completion?(nil) @@ -481,7 +499,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP public func loadNextReplies( after replyId: MessageId? = nil, limit: Int? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { if isLoadingNextReplies || hasLoadedAllNextReplies { completion?(nil) @@ -514,7 +532,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// Cleans the current state and loads the first page again. /// - Parameter limit: Limit for page size /// - Parameter completion: Callback when the API call is completed. - public func loadFirstPage(limit: Int? = nil, _ completion: ((_ error: Error?) -> Void)? = nil) { + public func loadFirstPage(limit: Int? = nil, _ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { let pageSize = limit ?? repliesPageSize messageUpdater.loadReplies( cid: cid, @@ -535,7 +553,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// called without an error and the delegate is notified of reactions changes. public func loadNextReactions( limit: Int = 25, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { if hasLoadedAllReactions { callback { completion?(nil) } @@ -583,7 +601,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP public func loadReactions( limit: Int, offset: Int = 0, - completion: @escaping (Result<[ChatMessageReaction], Error>) -> Void + completion: @escaping @Sendable(Result<[ChatMessageReaction], Error>) -> Void ) { messageUpdater.loadReactions( cid: cid, @@ -609,7 +627,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP public func flag( reason: String? = nil, extraData: [String: RawJSON]? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { messageUpdater.flagMessage(true, with: messageId, in: cid, reason: reason, extraData: extraData) { error in self.callback { @@ -622,7 +640,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// - public func unflag(completion: ((Error?) -> Void)? = nil) { + public func unflag(completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.flagMessage(false, with: messageId, in: cid, reason: nil, extraData: nil) { error in self.callback { completion?(error) @@ -642,7 +660,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP score: Int = 1, enforceUnique: Bool = false, extraData: [String: RawJSON] = [:], - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { messageUpdater.addReaction( type, @@ -663,7 +681,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. public func deleteReaction( _ type: MessageReactionType, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { messageUpdater.deleteReaction(type, messageId: messageId) { error in self.callback { @@ -676,7 +694,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// - Parameters: /// - pinning: The pinning expiration information. It supports setting an infinite expiration, setting a date, or the amount of time a message is pinned. /// - completion: A completion block with an error if the request was failed. - public func pin(_ pinning: MessagePinning, completion: ((Error?) -> Void)? = nil) { + public func pin(_ pinning: MessagePinning, completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.pinMessage(messageId: messageId, pinning: pinning) { result in self.callback { completion?(result.error) @@ -687,7 +705,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// Unpins the message this controller manages. /// - Parameters: /// - completion: A completion block with an error if the request was failed. - public func unpin(completion: ((Error?) -> Void)? = nil) { + public func unpin(completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.unpinMessage(messageId: messageId) { result in self.callback { completion?(result.error) @@ -704,7 +722,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// - Note: The local storage URL (`attachment.downloadingState?.localFileURL`) can change between app launches. public func downloadAttachment( _ attachment: ChatMessageAttachment, - completion: @escaping (Result, Error>) -> Void + completion: @escaping @Sendable(Result, Error>) -> Void ) where Payload: DownloadableAttachmentPayload { messageUpdater.downloadAttachment(attachment) { result in self.callback { @@ -720,7 +738,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// - Parameters: /// - attachmentId: The id of the attachment. /// - completion: A completion block with an error if the deletion failed. - public func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: ((Error?) -> Void)? = nil) { + public func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.deleteLocalAttachmentDownload(for: attachmentId) { error in self.callback { completion?(error) @@ -735,7 +753,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// If operation fails, the completion will be called with an error. public func restartFailedAttachmentUploading( with id: AttachmentId, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { messageUpdater.restartFailedAttachmentUploading(with: id) { error in self.callback { @@ -747,7 +765,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// Changes local message from `.sendingFailed` to `.pendingSend` so it is enqueued by message sender worker. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the database operation is finished. /// If operation fails, the completion will be called with an error. - public func resendMessage(completion: ((Error?) -> Void)? = nil) { + public func resendMessage(completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.resendMessage(with: messageId) { error in self.callback { completion?(error) @@ -760,7 +778,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// - action: The action to take. /// - completion: The completion. Will be called on a **callbackQueue** when the operation is finished. /// If operation fails, the completion is called with the error. - public func dispatchEphemeralMessageAction(_ action: AttachmentAction, completion: ((Error?) -> Void)? = nil) { + public func dispatchEphemeralMessageAction(_ action: AttachmentAction, completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.dispatchEphemeralMessageAction(cid: cid, messageId: messageId, action: action) { error in self.callback { completion?(error) @@ -775,7 +793,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// - language: The language message text should be translated to. /// - completion: The completion. Will be called on a **callbackQueue** when the operation is finished. /// If operation fails, the completion is called with the error. - public func translate(to language: TranslationLanguage, completion: ((Error?) -> Void)? = nil) { + public func translate(to language: TranslationLanguage, completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.translate(messageId: messageId, to: language) { result in self.callback { completion?(result.error) @@ -784,7 +802,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP } /// Marks the thread read if this message is the root of a thread. - public func markThreadRead(completion: ((Error?) -> Void)? = nil) { + public func markThreadRead(completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.markThreadRead(cid: cid, threadId: messageId) { error in self.callback { completion?(error) @@ -793,7 +811,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP } /// Marks the thread unread if this message is the root of a thread. - public func markThreadUnread(completion: ((Error?) -> Void)? = nil) { + public func markThreadUnread(completion: (@Sendable(Error?) -> Void)? = nil) { messageUpdater.markThreadUnread( cid: cid, threadId: messageId @@ -813,7 +831,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP public func loadThread( replyLimit: Int? = nil, participantLimit: Int? = nil, - completion: @escaping ((Result) -> Void) + completion: @escaping (@Sendable(Result) -> Void) ) { var query = ThreadQuery( messageId: messageId, @@ -841,7 +859,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP title: String?, extraData: [String: RawJSON]? = nil, unsetProperties: [String]? = nil, - completion: @escaping ((Result) -> Void) + completion: @escaping @Sendable(Result) -> Void ) { messageUpdater.updateThread( for: messageId, @@ -882,7 +900,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP showReplyInChannel: Bool = false, command: Command? = nil, extraData: [String: RawJSON] = [:], - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { draftsRepository.updateDraft( for: cid, @@ -907,7 +925,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP /// /// It is not necessary to call this method if the thread was loaded before. public func loadDraftReply( - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { draftsRepository.getDraft( for: cid, @@ -920,7 +938,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP } /// Deletes the draft message for this thread. - public func deleteDraftReply(completion: ((Error?) -> Void)? = nil) { + public func deleteDraftReply(completion: (@Sendable(Error?) -> Void)? = nil) { draftsRepository.deleteDraft( for: cid, threadId: messageId @@ -1060,7 +1078,7 @@ public extension ChatMessageController { } extension ClientError { - final class MessageEmptyReplies: ClientError { + final class MessageEmptyReplies: ClientError, @unchecked Sendable { override public var localizedDescription: String { "You can't load previous replies when there is no replies for the message." } diff --git a/Sources/StreamChat/Controllers/PollController/PollController.swift b/Sources/StreamChat/Controllers/PollController/PollController.swift index feda6f50340..f8c9a4293c2 100644 --- a/Sources/StreamChat/Controllers/PollController/PollController.swift +++ b/Sources/StreamChat/Controllers/PollController/PollController.swift @@ -17,7 +17,7 @@ public extension ChatClient { } } -public class PollController: DataController, DelegateCallable, DataStoreProvider { +public class PollController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The `ChatClient` instance this controller belongs to. public let client: ChatClient @@ -106,7 +106,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider return observer }() - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -138,7 +138,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider eventsController.delegate = self } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startObserversIfNeeded() pollsRepository.queryPollVotes(query: ownVotesQuery) { [weak self] result in @@ -161,7 +161,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider public func castPollVote( answerText: String?, optionId: String?, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { if answerText == nil && optionId == nil { completion?(ClientError.InvalidInput()) @@ -200,7 +200,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider /// - completion: A closure to be called upon completion, with an optional `Error` if something went wrong. public func removePollVote( voteId: String, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { pollsRepository.removePollVote( messageId: messageId, @@ -218,7 +218,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider /// /// - Parameters: /// - completion: A closure to be called upon completion, with an optional `Error` if something went wrong. - public func closePoll(completion: ((Error?) -> Void)? = nil) { + public func closePoll(completion: (@Sendable(Error?) -> Void)? = nil) { pollsRepository.closePoll(pollId: pollId, completion: { [weak self] result in self?.callback { completion?(result) @@ -237,7 +237,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider text: String, position: Int? = nil, extraData: [String: RawJSON]? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { pollsRepository.suggestPollOption( pollId: pollId, @@ -268,7 +268,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider } /// Represents the visibility of votes in a poll. -public struct VotingVisibility: RawRepresentable, Equatable { +public struct VotingVisibility: RawRepresentable, Equatable, Sendable { public let rawValue: String public init(rawValue: String) { diff --git a/Sources/StreamChat/Controllers/PollController/PollVoteListController.swift b/Sources/StreamChat/Controllers/PollController/PollVoteListController.swift index ab5d5f59942..1593bc6625c 100644 --- a/Sources/StreamChat/Controllers/PollController/PollVoteListController.swift +++ b/Sources/StreamChat/Controllers/PollController/PollVoteListController.swift @@ -29,7 +29,7 @@ public protocol PollVoteListControllerDelegate: DataControllerStateDelegate { } /// A controller which allows querying and filtering the votes of a poll. -public class PollVoteListController: DataController, DelegateCallable, DataStoreProvider { +public class PollVoteListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The query specifying and filtering the list of users. public let query: PollVoteListQuery @@ -46,7 +46,12 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore } /// A Boolean value that returns whether pagination is finished. - public private(set) var hasLoadedAllVotes: Bool = false + public private(set) var hasLoadedAllVotes: Bool { + get { queue.sync { _hasLoadedAllVotes } } + set { queue.sync { _hasLoadedAllVotes = newValue } } + } + + private var _hasLoadedAllVotes: Bool = false /// Set the delegate of `PollVoteListController` to observe the changes in the system. public weak var delegate: PollVoteListControllerDelegate? { @@ -89,7 +94,7 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore return observer }() - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -104,7 +109,12 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore private let eventsController: EventsController private let pollsRepository: PollsRepository private let environment: Environment - private var nextCursor: String? + private var nextCursor: String? { + get { queue.sync { _nextCursor } } + set { queue.sync { _nextCursor = newValue } } + } + + private var _nextCursor: String? /// Creates a new `PollVoteListController`. /// @@ -121,7 +131,7 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore eventsController.delegate = self } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startPollVotesListObserverIfNeeded() pollsRepository.queryPollVotes(query: query) { [weak self] result in @@ -162,7 +172,7 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore /// - completion: The completion callback. public func loadMoreVotes( limit: Int? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { let limit = limit ?? query.pagination.pageSize var updatedQuery = query diff --git a/Sources/StreamChat/Controllers/ReactionListController/ReactionListController.swift b/Sources/StreamChat/Controllers/ReactionListController/ReactionListController.swift index 767ed8ead32..c5a8d2160f5 100644 --- a/Sources/StreamChat/Controllers/ReactionListController/ReactionListController.swift +++ b/Sources/StreamChat/Controllers/ReactionListController/ReactionListController.swift @@ -21,7 +21,7 @@ public protocol ChatReactionListControllerDelegate: DataControllerStateDelegate /// A controller which allows querying and filtering the reactions of a message. /// /// - Note: For an async-await alternative of the `ChatReactionListController`, please check ``ReactionList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class ChatReactionListController: DataController, DelegateCallable, DataStoreProvider { +public class ChatReactionListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The query specifying and filtering the list of reactions. public let query: ReactionListQuery @@ -85,7 +85,7 @@ public class ChatReactionListController: DataController, DelegateCallable, DataS return observer }() - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -110,7 +110,7 @@ public class ChatReactionListController: DataController, DelegateCallable, DataS self.environment = environment } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startReactionListObserverIfNeeded() worker.loadReactions(query: query) { result in @@ -148,7 +148,7 @@ public extension ChatReactionListController { /// - completion: The completion callback. func loadMoreReactions( limit: Int = 25, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { var updatedQuery = query updatedQuery.pagination = Pagination(pageSize: limit, offset: reactions.count) diff --git a/Sources/StreamChat/Controllers/SearchControllers/MessageSearchController/MessageSearchController.swift b/Sources/StreamChat/Controllers/SearchControllers/MessageSearchController/MessageSearchController.swift index 6e462d34e2d..ba10e335c93 100644 --- a/Sources/StreamChat/Controllers/SearchControllers/MessageSearchController/MessageSearchController.swift +++ b/Sources/StreamChat/Controllers/SearchControllers/MessageSearchController/MessageSearchController.swift @@ -20,7 +20,7 @@ public extension ChatClient { /// `ChatMessageSearchController` is a controller class which allows observing a list of messages based on the provided query. /// /// - Note: For an async-await alternative of the `ChatMessageSearchController`, please check ``MessageSearch`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class ChatMessageSearchController: DataController, DelegateCallable, DataStoreProvider { +public class ChatMessageSearchController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The `ChatClient` instance this controller belongs to. public let client: ChatClient private let environment: Environment @@ -28,7 +28,12 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data init(client: ChatClient, environment: Environment = .init()) { self.client = client self.environment = environment - + messageUpdater = environment.messageUpdaterBuilder( + client.config.isLocalStorageEnabled, + client.messageRepository, + client.databaseContainer, + client.apiClient + ) super.init() setMessagesObserver() @@ -44,7 +49,12 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data /// Filter hash this controller observes. let explicitFilterHash = UUID().uuidString - private var nextPageCursor: String? + private var nextPageCursor: String? { + get { queue.sync { _nextPageCursor } } + set { queue.sync { _nextPageCursor = newValue } } + } + + private var _nextPageCursor: String? lazy var query: MessageSearchQuery = { // Filter is just a mock, explicit hash will override it @@ -55,7 +65,12 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data }() /// Copy of last search query made, used for getting next page. - var lastQuery: MessageSearchQuery? + var lastQuery: MessageSearchQuery? { + get { queue.sync { _lastQuery } } + set { queue.sync { _lastQuery = newValue } } + } + + private var _lastQuery: MessageSearchQuery? /// The messages matching the query of this controller. /// @@ -66,13 +81,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data return messagesObserver?.items ?? [] } - lazy var messageUpdater = self.environment - .messageUpdaterBuilder( - client.config.isLocalStorageEnabled, - client.messageRepository, - client.databaseContainer, - client.apiClient - ) + let messageUpdater: MessageUpdater /// Used for observing the database for changes. private var messagesObserver: BackgroundListDatabaseObserver? @@ -111,7 +120,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data messagesObserver = observer } - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -145,7 +154,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data /// - text: The message text. /// - completion: Called when the controller has finished fetching remote data. /// If the data fetching fails, the error variable contains more details about the problem. - public func search(text: String, completion: ((_ error: Error?) -> Void)? = nil) { + public func search(text: String, completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startObserversIfNeeded() guard let currentUserId = client.currentUserId else { @@ -191,7 +200,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data /// - query: Search query. /// - completion: Called when the controller has finished fetching remote data. /// If the data fetching fails, the error variable contains more details about the problem. - public func search(query: MessageSearchQuery, completion: ((_ error: Error?) -> Void)? = nil) { + public func search(query: MessageSearchQuery, completion: (@Sendable(_ error: Error?) -> Void)? = nil) { var query = query query.filterHash = explicitFilterHash @@ -222,7 +231,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data /// public func loadNextMessages( limit: Int = 25, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { guard let lastQuery = lastQuery else { completion?(ClientError("You should make a search before calling for next page.")) diff --git a/Sources/StreamChat/Controllers/SearchControllers/UserSearchController/UserSearchController.swift b/Sources/StreamChat/Controllers/SearchControllers/UserSearchController/UserSearchController.swift index 1ee78523bc0..dd6a729f2d3 100644 --- a/Sources/StreamChat/Controllers/SearchControllers/UserSearchController/UserSearchController.swift +++ b/Sources/StreamChat/Controllers/SearchControllers/UserSearchController/UserSearchController.swift @@ -20,18 +20,23 @@ extension ChatClient { /// `ChatUserSearchController` is a controller class which allows observing a list of chat users based on the provided query. /// /// - Note: For an async-await alternative of the `ChatUserSearchController`, please check ``UserSearch`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class ChatUserSearchController: DataController, DelegateCallable, DataStoreProvider { +public class ChatUserSearchController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The `ChatClient` instance this controller belongs to. public let client: ChatClient /// Copy of last search query made, used for getting next page. - public private(set) var query: UserListQuery? + public private(set) var query: UserListQuery? { + get { queue.sync { _query } } + set { queue.async { self._query = newValue } } + } + + private var _query: UserListQuery? /// The users matching the last query of this controller. private var _users: [ChatUser] = [] public var userArray: [ChatUser] { setLocalDataFetchedStateIfNeeded() - return _users + return queue.sync { _users } } @available(*, deprecated, message: "Please, switch to `userArray: [ChatUser]`") @@ -73,7 +78,7 @@ public class ChatUserSearchController: DataController, DelegateCallable, DataSto /// - term: Search term. If empty string or `nil`, all users are fetched. /// - completion: Called when the controller has finished fetching remote data. /// If the data fetching fails, the error variable contains more details about the problem. - public func search(term: String?, completion: ((_ error: Error?) -> Void)? = nil) { + public func search(term: String?, completion: (@Sendable(_ error: Error?) -> Void)? = nil) { fetch(.search(term: term), completion: completion) } @@ -88,7 +93,7 @@ public class ChatUserSearchController: DataController, DelegateCallable, DataSto /// - query: Search query. /// - completion: Called when the controller has finished fetching remote data. /// If the data fetching fails, the error variable contains more details about the problem. - public func search(query: UserListQuery, completion: ((_ error: Error?) -> Void)? = nil) { + public func search(query: UserListQuery, completion: (@Sendable(_ error: Error?) -> Void)? = nil) { fetch(query, completion: completion) } @@ -101,7 +106,7 @@ public class ChatUserSearchController: DataController, DelegateCallable, DataSto /// public func loadNextUsers( limit: Int = 25, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { guard let lastQuery = query else { completion?(ClientError("You should make a search before calling for next page.")) @@ -116,7 +121,7 @@ public class ChatUserSearchController: DataController, DelegateCallable, DataSto /// Clears the current search results. public func clearResults() { - _users = [] + queue.async { self._users = [] } } } @@ -126,7 +131,7 @@ private extension ChatUserSearchController { /// - Parameters: /// - query: The query to fetch. /// - completion: The completion that is triggered when the query is processed. - func fetch(_ query: UserListQuery, completion: ((Error?) -> Void)? = nil) { + func fetch(_ query: UserListQuery, completion: (@Sendable(Error?) -> Void)? = nil) { // TODO: Remove with the next major // // This is needed to make the delegate fire about state changes at the same time with the same @@ -136,7 +141,7 @@ private extension ChatUserSearchController { userQueryUpdater.fetch(userListQuery: query) { [weak self] result in switch result { case let .success(page): - self?.save(page: page) { loadedUsers in + self?.save(page: page) { [weak self] loadedUsers in let listChanges = self?.prepareListChanges( loadedPage: loadedUsers, updatePolicy: query.pagination?.offset == 0 ? .replace : .merge @@ -144,11 +149,11 @@ private extension ChatUserSearchController { self?.query = query if let listChanges = listChanges, let users = self?.userList(after: listChanges) { - self?._users = users + self?.queue.async { [weak self] in self?._users = users } } self?.state = .remoteDataFetched - self?.callback { + self?.callback { [weak self] in self?.multicastDelegate.invoke { guard let self = self, let listChanges = listChanges else { return } $0.controller(self, didChangeUsers: listChanges) @@ -168,16 +173,13 @@ private extension ChatUserSearchController { /// - Parameters: /// - page: The page of users fetched from the API. /// - completion: The completion that will be called with user models when database write is completed. - func save(page: UserListPayload, completion: @escaping ([ChatUser]) -> Void) { - var loadedUsers: [ChatUser] = [] - - client.databaseContainer.write({ session in - loadedUsers = page + func save(page: UserListPayload, completion: @escaping @Sendable([ChatUser]) -> Void) { + client.databaseContainer.write(converting: { session in + page .users .compactMap { try? session.saveUser(payload: $0).asModel() } - - }, completion: { _ in - completion(loadedUsers) + }, completion: { result in + completion(result.value ?? []) }) } diff --git a/Sources/StreamChat/Controllers/ThreadListController/ThreadListController.swift b/Sources/StreamChat/Controllers/ThreadListController/ThreadListController.swift index a582a66edb1..f3f8fffa374 100644 --- a/Sources/StreamChat/Controllers/ThreadListController/ThreadListController.swift +++ b/Sources/StreamChat/Controllers/ThreadListController/ThreadListController.swift @@ -20,7 +20,7 @@ public protocol ChatThreadListControllerDelegate: DataControllerStateDelegate { /// `ChatThreadListController` is a controller class which allows querying and /// observing the threads that the current user is participating. -public class ChatThreadListController: DataController, DelegateCallable, DataStoreProvider { +public class ChatThreadListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The query of the thread list. public let query: ThreadListQuery @@ -28,7 +28,12 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto public let client: ChatClient /// The cursor of the next page in case there is more data. - private var nextCursor: String? + private var nextCursor: String? { + get { queue.sync { _nextCursor } } + set { queue.sync { _nextCursor = newValue } } + } + + private var _nextCursor: String? /// The threads matching the query of this controller. /// @@ -40,14 +45,15 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto } /// The repository used to fetch the data from remote and local cache. - private lazy var threadsRepository: ThreadsRepository = self.environment - .threadsRepositoryBuilder( - client.databaseContainer, - client.apiClient - ) + private let threadsRepository: ThreadsRepository /// A Boolean value that returns whether pagination is finished. - public private(set) var hasLoadedAllThreads: Bool = false + public private(set) var hasLoadedAllThreads: Bool { + get { queue.sync { _hasLoadedAllThreads } } + set { queue.sync { _hasLoadedAllThreads = newValue } } + } + + private var _hasLoadedAllThreads: Bool = false /// A type-erased delegate. var multicastDelegate: MulticastDelegate = .init() { @@ -80,7 +86,7 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto return observer }() - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -103,16 +109,20 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto self.client = client self.query = query self.environment = environment + threadsRepository = environment.threadsRepositoryBuilder( + client.databaseContainer, + client.apiClient + ) } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startThreadListObserverIfNeeded() threadsRepository.loadThreads( query: query ) { [weak self] result in switch result { case let .success(threadListResponse): - self?.callback { + self?.callback { [weak self] in self?.state = .remoteDataFetched self?.nextCursor = threadListResponse.next self?.hasLoadedAllThreads = threadListResponse.next == nil @@ -134,7 +144,7 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto /// - completion: The completion. public func loadMoreThreads( limit: Int? = nil, - completion: ((Result<[ChatThread], Error>) -> Void)? = nil + completion: (@Sendable(Result<[ChatThread], Error>) -> Void)? = nil ) { let limit = limit ?? query.limit var updatedQuery = query @@ -143,7 +153,7 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto threadsRepository.loadThreads(query: updatedQuery) { [weak self] result in switch result { case let .success(threadListResponse): - self?.callback { + self?.callback { [weak self] in let threads = threadListResponse.threads self?.nextCursor = threadListResponse.next self?.hasLoadedAllThreads = threadListResponse.next == nil diff --git a/Sources/StreamChat/Controllers/UserController/UserController.swift b/Sources/StreamChat/Controllers/UserController/UserController.swift index 3721322bd78..f1a50b7ea41 100644 --- a/Sources/StreamChat/Controllers/UserController/UserController.swift +++ b/Sources/StreamChat/Controllers/UserController/UserController.swift @@ -19,7 +19,7 @@ public extension ChatClient { /// /// `ChatUserController` objects are lightweight, and they can be used for both, continuous data change observations, /// and for quick user actions (like mute/unmute). -public class ChatUserController: DataController, DelegateCallable, DataStoreProvider { +public class ChatUserController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The identifier of tge user this controller observes. public let userId: UserId @@ -46,23 +46,45 @@ public class ChatUserController: DataController, DelegateCallable, DataStoreProv } /// The worker used to fetch the remote data and communicate with servers. - private lazy var userUpdater = createUserUpdater() + private var userUpdater: UserUpdater { + queue.sync { + if let _userUpdater { + return _userUpdater + } + let updater = createUserUpdater() + _userUpdater = updater + return updater + } + } + + private var _userUpdater: UserUpdater? /// The observer used to track the user changes in the database. - private lazy var userObserver = createUserObserver() - .onChange { [weak self] change in - self?.delegateCallback { [weak self] in - guard let self = self else { - log.warning("Callback called while self is nil") - return + private var userObserver: BackgroundEntityDatabaseObserver { + queue.sync { + if let observer = _userObserver { + return observer + } + var observer = createUserObserver() + observer = observer.onChange { [weak self] change in + self?.delegateCallback { [weak self] in + guard let self = self else { + log.warning("Callback called while self is nil") + return + } + $0.userController(self, didUpdateUser: change) } - $0.userController(self, didUpdateUser: change) } + _userObserver = observer + return observer } + } + + private var _userObserver: BackgroundEntityDatabaseObserver? private let environment: Environment - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -89,7 +111,7 @@ public class ChatUserController: DataController, DelegateCallable, DataStoreProv self.environment = environment } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startObservingIfNeeded() if case let .localDataFetchFailed(error) = state { @@ -140,7 +162,7 @@ public extension ChatUserController { /// Mutes the user this controller manages. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - func mute(completion: ((Error?) -> Void)? = nil) { + func mute(completion: (@Sendable(Error?) -> Void)? = nil) { userUpdater.muteUser(userId) { error in self.callback { completion?(error) @@ -151,7 +173,7 @@ public extension ChatUserController { /// Unmutes the user this controller manages. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// - func unmute(completion: ((Error?) -> Void)? = nil) { + func unmute(completion: (@Sendable(Error?) -> Void)? = nil) { userUpdater.unmuteUser(userId) { error in self.callback { completion?(error) @@ -162,7 +184,7 @@ public extension ChatUserController { /// Blocks the user this controller manages. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// If request fails, the completion will be called with an error. - func block(completion: ((Error?) -> Void)? = nil) { + func block(completion: (@Sendable(Error?) -> Void)? = nil) { userUpdater.blockUser(userId) { error in self.callback { completion?(error) @@ -173,7 +195,7 @@ public extension ChatUserController { /// Unblocks the user this controller manages. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// - func unblock(completion: ((Error?) -> Void)? = nil) { + func unblock(completion: (@Sendable(Error?) -> Void)? = nil) { userUpdater.unblockUser(userId) { error in self.callback { completion?(error) @@ -191,7 +213,7 @@ public extension ChatUserController { func flag( reason: String? = nil, extraData: [String: RawJSON]? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { userUpdater.flagUser(true, with: userId, reason: reason, extraData: extraData) { error in self.callback { @@ -203,7 +225,7 @@ public extension ChatUserController { /// Unflags the user this controller manages. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. /// - func unflag(completion: ((Error?) -> Void)? = nil) { + func unflag(completion: (@Sendable(Error?) -> Void)? = nil) { userUpdater.flagUser(false, with: userId, reason: nil, extraData: nil) { error in self.callback { completion?(error) diff --git a/Sources/StreamChat/Controllers/UserListController/UserListController.swift b/Sources/StreamChat/Controllers/UserListController/UserListController.swift index 634fac7e08d..e844fa00c52 100644 --- a/Sources/StreamChat/Controllers/UserListController/UserListController.swift +++ b/Sources/StreamChat/Controllers/UserListController/UserListController.swift @@ -20,7 +20,7 @@ extension ChatClient { /// `ChatUserListController` is a controller class which allows observing a list of chat users based on the provided query. /// /// - Note: For an async-await alternative of the `ChatUserListController`, please check ``UserList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). -public class ChatUserListController: DataController, DelegateCallable, DataStoreProvider { +public class ChatUserListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { /// The query specifying and filtering the list of users. public let query: UserListQuery @@ -38,11 +38,7 @@ public class ChatUserListController: DataController, DelegateCallable, DataStore } /// The worker used to fetch the remote data and communicate with servers. - private lazy var worker: UserListUpdater = self.environment - .userQueryUpdaterBuilder( - client.databaseContainer, - client.apiClient - ) + private let worker: UserListUpdater /// A type-erased delegate. var multicastDelegate: MulticastDelegate = .init() { @@ -79,7 +75,7 @@ public class ChatUserListController: DataController, DelegateCallable, DataStore return observer }() - var _basePublishers: Any? + @Atomic private var _basePublishers: Any? /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose /// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally, /// and expose the published values by mapping them to a read-only `AnyPublisher` type. @@ -102,9 +98,13 @@ public class ChatUserListController: DataController, DelegateCallable, DataStore self.client = client self.query = query self.environment = environment + worker = environment.userQueryUpdaterBuilder( + client.databaseContainer, + client.apiClient + ) } - override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) { + override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) { startUserListObserverIfNeeded() worker.update(userListQuery: query) { result in @@ -143,7 +143,7 @@ public extension ChatUserListController { /// func loadNextUsers( limit: Int = 25, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { var updatedQuery = query updatedQuery.pagination = Pagination(pageSize: limit, offset: users.count) diff --git a/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift b/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift index af60ce40118..5dd92beb995 100644 --- a/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift +++ b/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift @@ -86,7 +86,7 @@ class AttachmentDTO: NSManagedObject { } static func load(id: AttachmentId, context: NSManagedObjectContext) -> AttachmentDTO? { - load(by: id.rawValue, context: context).first + load(by: id.rawValue, context: context).first as? Self } static func loadOrCreate(id: AttachmentId, context: NSManagedObjectContext) -> AttachmentDTO { @@ -374,27 +374,27 @@ extension LocalAttachmentDownloadState { } extension ClientError { - final class AttachmentDoesNotExist: ClientError { + final class AttachmentDoesNotExist: ClientError, @unchecked Sendable { init(id: AttachmentId) { super.init("There is no `AttachmentDTO` instance in the DB matching id: \(id).") } } - final class AttachmentUploadBlocked: ClientError { + final class AttachmentUploadBlocked: ClientError, @unchecked Sendable { init(id: AttachmentId, attachmentType: AttachmentType, pathExtension: String) { super.init("`AttachmentDTO` with \(id) and type \(attachmentType) and path extension \(pathExtension) is blocked on the Stream dashboard.") } } - final class AttachmentEditing: ClientError { + final class AttachmentEditing: ClientError, @unchecked Sendable { init(id: AttachmentId, reason: String) { super.init("`AttachmentDTO` with id: \(id) can't be edited (\(reason))") } } - final class AttachmentDecoding: ClientError {} + final class AttachmentDecoding: ClientError, @unchecked Sendable {} - final class AttachmentDownloading: ClientError { + final class AttachmentDownloading: ClientError, @unchecked Sendable { init(id: AttachmentId, reason: String) { super.init( "Failed to download attachment with id: \(id): \(reason)" @@ -402,7 +402,7 @@ extension ClientError { } } - final class AttachmentUploading: ClientError { + final class AttachmentUploading: ClientError, @unchecked Sendable { init(id: AttachmentId) { super.init( "Failed to upload attachment with id: \(id)" diff --git a/Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift index 2265f3d99ab..11c68b44d84 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift @@ -21,7 +21,7 @@ class ChannelListQueryDTO: NSManagedObject { keyPath: #keyPath(ChannelListQueryDTO.filterHash), equalTo: filterHash, context: context - ).first + ).first as? Self } /// The fetch request that returns all existed queries from the database. diff --git a/Sources/StreamChat/Database/DTOs/ChannelMemberListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelMemberListQueryDTO.swift index ced9facedd4..5f53fd47668 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelMemberListQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelMemberListQueryDTO.swift @@ -23,7 +23,7 @@ final class ChannelMemberListQueryDTO: NSManagedObject { keyPath: #keyPath(ChannelMemberListQueryDTO.queryHash), equalTo: queryHash, context: context - ).first + ).first as? Self } static func loadOrCreate(queryHash: String, context: NSManagedObjectContext) -> ChannelMemberListQueryDTO { diff --git a/Sources/StreamChat/Database/DTOs/DeviceDTO.swift b/Sources/StreamChat/Database/DTOs/DeviceDTO.swift index f4d3098b789..dd0e32a2cd5 100644 --- a/Sources/StreamChat/Database/DTOs/DeviceDTO.swift +++ b/Sources/StreamChat/Database/DTOs/DeviceDTO.swift @@ -21,7 +21,7 @@ extension DeviceDTO { /// - context: The context used to fetch `DeviceDTO` /// static func load(id: String, context: NSManagedObjectContext) -> DeviceDTO? { - load(by: id, context: context).first + load(by: id, context: context).first as? Self } /// If a Device with the given id exists in the context, fetches and returns it. Otherwise creates a new diff --git a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift index 9694d71045d..1d81c18dffc 100644 --- a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift +++ b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift @@ -74,7 +74,7 @@ extension MemberDTO { } static func load(memberId: String, context: NSManagedObjectContext) -> MemberDTO? { - load(by: memberId, context: context).first + load(by: memberId, context: context).first as? Self } /// If a User with the given id exists in the context, fetches and returns it. Otherwise create a new diff --git a/Sources/StreamChat/Database/DTOs/MessageDTO.swift b/Sources/StreamChat/Database/DTOs/MessageDTO.swift index 83c8ff2bfe5..c949b4d1017 100644 --- a/Sources/StreamChat/Database/DTOs/MessageDTO.swift +++ b/Sources/StreamChat/Database/DTOs/MessageDTO.swift @@ -483,7 +483,7 @@ class MessageDTO: NSManagedObject { } static func load(id: String, context: NSManagedObjectContext) -> MessageDTO? { - load(by: id, context: context).first + load(by: id, context: context).first as? Self } static func loadOrCreate(id: String, context: NSManagedObjectContext, cache: PreWarmedCache?) -> MessageDTO { @@ -1799,16 +1799,16 @@ private extension ChatMessage { } extension ClientError { - final class CurrentUserDoesNotExist: ClientError { + final class CurrentUserDoesNotExist: ClientError, @unchecked Sendable { override var localizedDescription: String { "There is no `CurrentUserDTO` instance in the DB." + "Make sure to call `client.currentUserController.reloadUserIfNeeded()`" } } - final class MessagePayloadSavingFailure: ClientError {} + final class MessagePayloadSavingFailure: ClientError, @unchecked Sendable {} - final class ChannelDoesNotExist: ClientError { + final class ChannelDoesNotExist: ClientError, @unchecked Sendable { init(cid: ChannelId) { super.init("There is no `ChannelDTO` instance in the DB matching cid: \(cid).") } diff --git a/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift b/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift index 7d6d9d6ece5..115c15b5313 100644 --- a/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift +++ b/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift @@ -66,17 +66,17 @@ extension MessageReactionDTO { } static func load(reactionId: String, context: NSManagedObjectContext) -> MessageReactionDTO? { - load(by: reactionId, context: context).first + load(by: reactionId, context: context).first as? Self } - static let notLocallyDeletedPredicates: NSPredicate = { + static var notLocallyDeletedPredicates: NSPredicate { NSCompoundPredicate(orPredicateWithSubpredicates: [ NSPredicate(format: "localStateRaw == %@", LocalReactionState.unknown.rawValue), NSPredicate(format: "localStateRaw == %@", LocalReactionState.sending.rawValue), NSPredicate(format: "localStateRaw == %@", LocalReactionState.pendingSend.rawValue), NSPredicate(format: "localStateRaw == %@", LocalReactionState.deletingFailed.rawValue) ]) - }() + } static func loadReactions(ids: [String], context: NSManagedObjectContext) -> [MessageReactionDTO] { guard !ids.isEmpty else { diff --git a/Sources/StreamChat/Database/DTOs/MessageSearchQueryDTO.swift b/Sources/StreamChat/Database/DTOs/MessageSearchQueryDTO.swift index 50284afb7ac..5af2f13f826 100644 --- a/Sources/StreamChat/Database/DTOs/MessageSearchQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/MessageSearchQueryDTO.swift @@ -16,7 +16,7 @@ class MessageSearchQueryDTO: NSManagedObject { keyPath: #keyPath(MessageSearchQueryDTO.filterHash), equalTo: filterHash, context: context - ).first + ).first as? Self } } diff --git a/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift index b1d9338cc01..94a19a60632 100644 --- a/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift @@ -21,7 +21,7 @@ class PollVoteListQueryDTO: NSManagedObject { keyPath: #keyPath(PollVoteListQueryDTO.filterHash), equalTo: filterHash, context: context - ).first + ).first as? Self } static func loadOrCreate(filterHash: String, context: NSManagedObjectContext) -> PollVoteListQueryDTO { diff --git a/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift index dbb93e750ae..1c21431ce44 100644 --- a/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift @@ -21,7 +21,7 @@ class ReactionListQueryDTO: NSManagedObject { keyPath: #keyPath(ReactionListQueryDTO.filterHash), equalTo: filterHash, context: context - ).first + ).first as? Self } static func loadOrCreate(filterHash: String, context: NSManagedObjectContext) -> ReactionListQueryDTO { diff --git a/Sources/StreamChat/Database/DTOs/UserDTO.swift b/Sources/StreamChat/Database/DTOs/UserDTO.swift index 476d1abc3f5..ab64cc7fbcb 100644 --- a/Sources/StreamChat/Database/DTOs/UserDTO.swift +++ b/Sources/StreamChat/Database/DTOs/UserDTO.swift @@ -106,7 +106,7 @@ extension UserDTO { /// - context: The context used to fetch `UserDTO` /// static func load(id: String, context: NSManagedObjectContext) -> UserDTO? { - load(by: id, context: context).first + load(by: id, context: context).first as? Self } /// If a User with the given id exists in the context, fetches and returns it. Otherwise creates a new diff --git a/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift index 92958a353b9..e9d0a5dbe02 100644 --- a/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift +++ b/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift @@ -25,7 +25,7 @@ class UserListQueryDTO: NSManagedObject { keyPath: #keyPath(UserListQueryDTO.filterHash), equalTo: filterHash, context: context - ).first + ).first as? Self } } diff --git a/Sources/StreamChat/Database/DatabaseContainer.swift b/Sources/StreamChat/Database/DatabaseContainer.swift index 0648a5779a6..4016ca719eb 100644 --- a/Sources/StreamChat/Database/DatabaseContainer.swift +++ b/Sources/StreamChat/Database/DatabaseContainer.swift @@ -2,7 +2,7 @@ // Copyright © 2025 Stream.io Inc. All rights reserved. // -import CoreData +@preconcurrency import CoreData import Foundation /// Convenience subclass of `NSPersistentContainer` allowing easier setup of the database stack. @@ -70,7 +70,8 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { let chatClientConfig: ChatClientConfig - static var cachedModels = [String: NSManagedObjectModel]() + private static let cachedModelsQueue = DispatchQueue(label: "io.getstream.database-container", target: .global()) + private nonisolated(unsafe) static var _cachedModels = [String: NSManagedObjectModel]() /// All `NSManagedObjectContext`s this container owns. private(set) lazy var allContext: [NSManagedObjectContext] = [viewContext, backgroundReadOnlyContext, stateLayerContext, writableContext] @@ -94,7 +95,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { chatClientConfig: ChatClientConfig ) { let managedObjectModel: NSManagedObjectModel - if let cachedModel = Self.cachedModels[modelName] { + if let cachedModel = Self.cachedModelsQueue.sync(execute: { Self._cachedModels[modelName] }) { managedObjectModel = cachedModel } else { // It's safe to unwrap the following values because this is not settable by users and it's always a programmer error. @@ -102,7 +103,9 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { let modelURL = bundle.url(forResource: modelName, withExtension: "momd")! let model = NSManagedObjectModel(contentsOf: modelURL)! managedObjectModel = model - Self.cachedModels[modelName] = model + Self.cachedModelsQueue.async { + Self._cachedModels[modelName] = model + } } self.chatClientConfig = chatClientConfig @@ -173,7 +176,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { /// Use this method to safely mutate the content of the database. This method is asynchronous. /// /// - Parameter actions: A block that performs the actual mutation. - func write(_ actions: @escaping (DatabaseSession) throws -> Void) { + func write(_ actions: @escaping @Sendable(DatabaseSession) throws -> Void) { write(actions, completion: { _ in }) } @@ -185,7 +188,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { /// - Parameters: /// - actions: A block that performs the actual mutation. /// - completion: Called when the changes are saved to the DB. If the changes can't be saved, called with an error. - func write(_ actions: @escaping (DatabaseSession) throws -> Void, completion: @escaping (Error?) -> Void) { + func write(_ actions: @escaping @Sendable(DatabaseSession) throws -> Void, completion: @escaping @Sendable(Error?) -> Void) { writableContext.perform { log.debug("Starting a database session.", subsystems: .database) do { @@ -211,7 +214,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } } - func write(_ actions: @escaping (DatabaseSession) throws -> Void) async throws { + func write(_ actions: @escaping @Sendable(DatabaseSession) throws -> Void) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in write(actions) { error in if let error { @@ -223,23 +226,34 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } } - func write(converting actions: @escaping (DatabaseSession) throws -> T, completion: @escaping (Result) -> Void) { - var result: T? + func write(converting actions: @escaping @Sendable(DatabaseSession) throws -> T, completion: @escaping @Sendable(Result) -> Void) where T: Sendable { + nonisolated(unsafe) var result: T? write { session in result = try actions(session) } completion: { error in - if let result { + // Note: actions can pass which returns a value, but underlying write can fail + if let error { + completion(.failure(error)) + } else if let result { completion(.success(result)) } else { - completion(.failure(error ?? ClientError.Unknown())) + completion(.failure(ClientError.Unknown())) + } + } + } + + func writeConverting(_ actions: @escaping @Sendable(DatabaseSession) throws -> T) async throws -> T where T: Sendable { + try await withCheckedThrowingContinuation { continuation in + write(converting: actions) { result in + continuation.resume(with: result) } } } private func read( from context: NSManagedObjectContext, - _ actions: @escaping (DatabaseSession) throws -> T, - completion: @escaping (Result) -> Void + _ actions: @escaping @Sendable(DatabaseSession) throws -> T, + completion: @escaping @Sendable(Result) -> Void ) { context.perform { do { @@ -255,11 +269,11 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } } - func read(_ actions: @escaping (DatabaseSession) throws -> T, completion: @escaping (Result) -> Void) { + func read(_ actions: @escaping @Sendable(DatabaseSession) throws -> T, completion: @escaping @Sendable(Result) -> Void) { read(from: backgroundReadOnlyContext, actions, completion: completion) } - func read(_ actions: @escaping (DatabaseSession) throws -> T) async throws -> T { + func read(_ actions: @escaping @Sendable(DatabaseSession) throws -> T) async throws -> T where T: Sendable { try await withCheckedThrowingContinuation { continuation in read(from: stateLayerContext, actions) { result in continuation.resume(with: result) diff --git a/Sources/StreamChat/Errors/ClientError.swift b/Sources/StreamChat/Errors/ClientError.swift index bc8d066cc39..4d664daddff 100644 --- a/Sources/StreamChat/Errors/ClientError.swift +++ b/Sources/StreamChat/Errors/ClientError.swift @@ -5,7 +5,7 @@ import Foundation /// A Client error. -public class ClientError: Error, CustomStringConvertible { +public class ClientError: Error, CustomStringConvertible, @unchecked Sendable { public struct Location: Equatable { public let file: String public let line: Int @@ -56,10 +56,10 @@ public class ClientError: Error, CustomStringConvertible { extension ClientError { /// An unexpected error. - public final class Unexpected: ClientError {} + public final class Unexpected: ClientError, @unchecked Sendable {} /// An unknown error. - public final class Unknown: ClientError {} + public final class Unknown: ClientError, @unchecked Sendable {} } // This should probably live only in the test target since it's not "true" equatable diff --git a/Sources/StreamChat/Extensions/Task+Extensions.swift b/Sources/StreamChat/Extensions/Task+Extensions.swift index a142f2d575e..bbd732f6c0a 100644 --- a/Sources/StreamChat/Extensions/Task+Extensions.swift +++ b/Sources/StreamChat/Extensions/Task+Extensions.swift @@ -5,7 +5,7 @@ import Foundation extension Task { - @discardableResult static func mainActor(priority: TaskPriority? = nil, operation: @escaping @MainActor() async throws -> Success) -> Task where Failure == any Error { + @discardableResult static func mainActor(priority: TaskPriority? = nil, operation: @escaping @MainActor @Sendable() async throws -> Success) -> Task where Failure == any Error { Task(priority: priority) { @MainActor in try await operation() } diff --git a/Sources/StreamChat/Models/AppSettings.swift b/Sources/StreamChat/Models/AppSettings.swift index d3cd113fdf0..341e4a46139 100644 --- a/Sources/StreamChat/Models/AppSettings.swift +++ b/Sources/StreamChat/Models/AppSettings.swift @@ -6,7 +6,7 @@ import CoreServices import Foundation /// A type representing the app settings. -public struct AppSettings { +public struct AppSettings: Sendable { /// The name of the app. public let name: String /// The the file uploading configuration. @@ -18,7 +18,7 @@ public struct AppSettings { /// A boolean value determining if async url enrichment is enabled. public let asyncUrlEnrichEnabled: Bool - public struct UploadConfig { + public struct UploadConfig: Sendable { /// The allowed file extensions. public let allowedFileExtensions: [String] /// The blocked file extensions. diff --git a/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift b/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift index 217fa503013..808cab5ec64 100644 --- a/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift +++ b/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift @@ -6,7 +6,7 @@ import Foundation /// A protocol an attachment payload type has to conform in order it can be /// attached to/exposed on the message. -public protocol AttachmentPayload: Codable { +public protocol AttachmentPayload: Codable, Sendable { /// A type of resulting attachment. static var type: AttachmentType { get } } @@ -14,12 +14,12 @@ public protocol AttachmentPayload: Codable { /// A type-erased type that wraps either a local file URL that has to be uploaded /// and attached to the message OR a custom payload which the message attachment /// should contain. -public struct AnyAttachmentPayload { +public struct AnyAttachmentPayload: Sendable { /// A type of attachment that will be created when the message is sent. public let type: AttachmentType /// A payload that will exposed on attachment when the message is sent. - public let payload: Encodable + public let payload: (Encodable & Sendable) /// A URL referencing to the local file that should be uploaded. public let localFileURL: URL? @@ -104,7 +104,7 @@ public extension AnyAttachmentPayload { localFileURL: URL, attachmentType: AttachmentType, localMetadata: AnyAttachmentLocalMetadata? = nil, - extraData: Encodable? = nil + extraData: (Encodable & Sendable)? = nil ) throws { let file = try AttachmentFile(url: localFileURL) let extraData = try extraData @@ -166,7 +166,7 @@ public extension AnyAttachmentPayload { } extension ClientError { - public final class UnsupportedUploadableAttachmentType: ClientError { + public final class UnsupportedUploadableAttachmentType: ClientError, @unchecked Sendable { init(_ type: AttachmentType) { super.init( "For uploadable attachments only image/video/audio/file/voiceRecording types are supported." diff --git a/Sources/StreamChat/Models/Attachments/AnyAttachmentUpdater.swift b/Sources/StreamChat/Models/Attachments/AnyAttachmentUpdater.swift index b7d4fa84a1a..755215e9c73 100644 --- a/Sources/StreamChat/Models/Attachments/AnyAttachmentUpdater.swift +++ b/Sources/StreamChat/Models/Attachments/AnyAttachmentUpdater.swift @@ -5,7 +5,7 @@ import Foundation /// Helper component to update the payload of a type-erased attachment. -public struct AnyAttachmentUpdater { +public struct AnyAttachmentUpdater: Sendable { public init() {} /// Updates the underlying payload of a type-erased attachment. diff --git a/Sources/StreamChat/Models/Attachments/AttachmentId.swift b/Sources/StreamChat/Models/Attachments/AttachmentId.swift index d6cc57fdea7..0c2f05fb6bf 100644 --- a/Sources/StreamChat/Models/Attachments/AttachmentId.swift +++ b/Sources/StreamChat/Models/Attachments/AttachmentId.swift @@ -5,7 +5,7 @@ import Foundation /// An object that uniquely identifies a message attachment. -public struct AttachmentId: Hashable { +public struct AttachmentId: Hashable, Sendable { /// The cid of the channel the attachment belongs to. public let cid: ChannelId diff --git a/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift b/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift index d1941d4cdab..25f470c8eeb 100644 --- a/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift +++ b/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift @@ -23,7 +23,7 @@ enum AttachmentCodingKeys: String, CodingKey, CaseIterable { } /// A local state of the attachment. Applies only for attachments linked to the new messages sent from current device. -public enum LocalAttachmentState: Hashable { +public enum LocalAttachmentState: Hashable, Sendable { /// The current state is unknown case unknown /// The attachment is waiting to be uploaded. @@ -37,7 +37,7 @@ public enum LocalAttachmentState: Hashable { } /// A local download state of the attachment. -public enum LocalAttachmentDownloadState: Hashable { +public enum LocalAttachmentDownloadState: Hashable, Sendable { /// The attachment is being downloaded. case downloading(progress: Double) /// The attachment download failed. @@ -47,7 +47,7 @@ public enum LocalAttachmentDownloadState: Hashable { } /// An attachment action, e.g. send, shuffle. -public struct AttachmentAction: Codable, Hashable { +public struct AttachmentAction: Codable, Hashable, Sendable { /// A name. public let name: String /// A value of an action. @@ -84,12 +84,12 @@ public struct AttachmentAction: Codable, Hashable { public var isCancel: Bool { value.lowercased() == "cancel" } /// An attachment action type, e.g. button. - public enum ActionType: String, Codable { + public enum ActionType: String, Codable, Sendable { case button } /// An attachment action style, e.g. primary button. - public enum ActionStyle: String, Codable { + public enum ActionStyle: String, Codable, Sendable { case `default` case primary } @@ -97,7 +97,7 @@ public struct AttachmentAction: Codable, Hashable { /// An attachment type. /// There are some predefined types on backend but any type can be introduced and sent to backend. -public struct AttachmentType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral { +public struct AttachmentType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable { public let rawValue: String public init(rawValue: String) { @@ -140,7 +140,7 @@ public extension AttachmentType { } /// An attachment file description. -public struct AttachmentFile: Codable, Hashable { +public struct AttachmentFile: Codable, Hashable, Sendable { enum CodingKeys: String, CodingKey, CaseIterable { case mimeType = "mime_type" case size = "file_size" @@ -153,7 +153,7 @@ public struct AttachmentFile: Codable, Hashable { /// A mime type. public let mimeType: String? /// A file size formatter. - public static let sizeFormatter = ByteCountFormatter() + public static var sizeFormatter: ByteCountFormatter { ByteCountFormatter() } // TODO: This should be deprecated in the future. UI Formatting should not belong to domain models. // All formatting logic should come from `Appearance.formatters`. @@ -213,7 +213,7 @@ public struct AttachmentFile: Codable, Hashable { } /// An attachment file type. -public enum AttachmentFileType: String, Codable, Equatable, CaseIterable { +public enum AttachmentFileType: String, Codable, Equatable, CaseIterable, Sendable { /// File case generic, doc, docx, pdf, ppt, pptx, tar, xls, zip, x7z, xz, ods, odt, xlsx /// Text @@ -318,7 +318,7 @@ public enum AttachmentFileType: String, Codable, Equatable, CaseIterable { } extension ClientError { - final class InvalidAttachmentFileURL: ClientError { + final class InvalidAttachmentFileURL: ClientError, @unchecked Sendable { init(_ url: URL) { super.init("The \(url) is invalid since it is not a file URL.") } diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift index d7baa0ce87a..39528a44643 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift @@ -53,9 +53,10 @@ public extension ChatMessageAttachment { extension ChatMessageAttachment: Equatable where Payload: Equatable {} extension ChatMessageAttachment: Hashable where Payload: Hashable {} +extension ChatMessageAttachment: Sendable where Payload: Sendable {} /// A type represeting the downloading state for attachments. -public struct AttachmentDownloadingState: Hashable { +public struct AttachmentDownloadingState: Hashable, Sendable { /// The local file URL of the downloaded attachment. /// /// - Note: The local file URL is available when the state is `.downloaded`. @@ -72,7 +73,7 @@ public struct AttachmentDownloadingState: Hashable { } /// A type representing the uploading state for attachments that require prior uploading. -public struct AttachmentUploadingState: Hashable { +public struct AttachmentUploadingState: Hashable, Sendable { /// The local file URL that is being uploaded. public let localFileURL: URL diff --git a/Sources/StreamChat/Models/BanEnabling.swift b/Sources/StreamChat/Models/BanEnabling.swift index 85640e4da47..bbe19396c4c 100644 --- a/Sources/StreamChat/Models/BanEnabling.swift +++ b/Sources/StreamChat/Models/BanEnabling.swift @@ -3,7 +3,7 @@ // /// An option to enable ban users. -public enum BanEnabling { +public enum BanEnabling: Sendable { /// Disabled for everyone. case disabled diff --git a/Sources/StreamChat/Models/BlockedUserDetails.swift b/Sources/StreamChat/Models/BlockedUserDetails.swift index ccc908db837..52ee1787821 100644 --- a/Sources/StreamChat/Models/BlockedUserDetails.swift +++ b/Sources/StreamChat/Models/BlockedUserDetails.swift @@ -5,7 +5,7 @@ import Foundation /// A type representing a blocked user. -public struct BlockedUserDetails { +public struct BlockedUserDetails: Sendable { /// The unique identifier of the blocked user. public let userId: UserId diff --git a/Sources/StreamChat/Models/Channel.swift b/Sources/StreamChat/Models/Channel.swift index 77f811bba60..41ae3291d81 100644 --- a/Sources/StreamChat/Models/Channel.swift +++ b/Sources/StreamChat/Models/Channel.swift @@ -7,7 +7,7 @@ import Foundation /// A type representing a chat channel. `ChatChannel` is an immutable snapshot of a channel entity at the given time. /// -public struct ChatChannel { +public struct ChatChannel: Sendable { /// The `ChannelId` of the channel. public let cid: ChannelId @@ -331,7 +331,7 @@ extension ChatChannel: Hashable { } /// A struct describing unread counts for a channel. -public struct ChannelUnreadCount: Decodable, Equatable { +public struct ChannelUnreadCount: Decodable, Equatable, Sendable { /// The default value representing no unread messages. public static let noUnread = ChannelUnreadCount(messages: 0, mentions: 0) @@ -353,7 +353,7 @@ public extension ChannelUnreadCount { } /// An action that can be performed in a channel. -public struct ChannelCapability: RawRepresentable, ExpressibleByStringLiteral, Hashable { +public struct ChannelCapability: RawRepresentable, ExpressibleByStringLiteral, Hashable, Sendable { public var rawValue: String public init?(rawValue: String) { diff --git a/Sources/StreamChat/Models/ChannelId.swift b/Sources/StreamChat/Models/ChannelId.swift index b2ffd0ccac5..cf0b5ac68b7 100644 --- a/Sources/StreamChat/Models/ChannelId.swift +++ b/Sources/StreamChat/Models/ChannelId.swift @@ -69,7 +69,7 @@ extension ChannelId: Codable { } extension ClientError { - public final class InvalidChannelId: ClientError {} + public final class InvalidChannelId: ClientError, @unchecked Sendable {} } extension ChannelId: APIPathConvertible { diff --git a/Sources/StreamChat/Models/ChannelRead.swift b/Sources/StreamChat/Models/ChannelRead.swift index fa034ecfe0a..09e51c6fc7c 100644 --- a/Sources/StreamChat/Models/ChannelRead.swift +++ b/Sources/StreamChat/Models/ChannelRead.swift @@ -5,7 +5,7 @@ import Foundation /// A type representing a user's last read action on a channel. -public struct ChatChannelRead: Equatable { +public struct ChatChannelRead: Equatable, Sendable { /// The last time the user has read the channel. public let lastReadAt: Date diff --git a/Sources/StreamChat/Models/ChannelType.swift b/Sources/StreamChat/Models/ChannelType.swift index f3016284224..b43baa01046 100644 --- a/Sources/StreamChat/Models/ChannelType.swift +++ b/Sources/StreamChat/Models/ChannelType.swift @@ -5,7 +5,7 @@ import Foundation /// An enum describing possible types of a channel. -public enum ChannelType: Codable, Hashable { +public enum ChannelType: Codable, Hashable, Sendable { /// Sensible defaults in case you want to build livestream chat like Instagram Livestream or Periscope. case livestream diff --git a/Sources/StreamChat/Models/ChatMessage.swift b/Sources/StreamChat/Models/ChatMessage.swift index f75be99d7f1..ce8123d24fb 100644 --- a/Sources/StreamChat/Models/ChatMessage.swift +++ b/Sources/StreamChat/Models/ChatMessage.swift @@ -9,7 +9,7 @@ import Foundation public typealias MessageId = String /// A type representing a chat message. `ChatMessage` is an immutable snapshot of a chat message entity at the given time. -public struct ChatMessage { +public struct ChatMessage: Sendable { /// A unique identifier of the message. public let id: MessageId @@ -61,8 +61,8 @@ public struct ChatMessage { /// /// If message is inline reply this property will contain the message quoted by this reply. /// - public var quotedMessage: ChatMessage? { _quotedMessage() } - let _quotedMessage: () -> ChatMessage? + public var quotedMessage: ChatMessage? { _quotedMessage?.value as? ChatMessage } + let _quotedMessage: BoxedAny? /// The draft reply to this message. Applies only for the messages of the current user. public let draftReply: DraftMessage? @@ -257,7 +257,7 @@ public struct ChatMessage { self.currentUserReactions = currentUserReactions self.readBy = readBy _attachments = attachments - _quotedMessage = { quotedMessage } + _quotedMessage = BoxedAny(quotedMessage) self.draftReply = draftReply } @@ -447,7 +447,7 @@ extension ChatMessage: Hashable { } /// A type of the message. -public enum MessageType: String, Codable { +public enum MessageType: String, Codable, Sendable { /// A regular message created in the channel. case regular @@ -471,7 +471,7 @@ public enum MessageType: String, Codable { } // The pinning information of a message. -public struct MessagePinDetails { +public struct MessagePinDetails: Sendable { /// Date when the message got pinned public let pinnedAt: Date @@ -483,7 +483,7 @@ public struct MessagePinDetails { } /// A possible additional local state of the message. Applies only for the messages of the current user. -public enum LocalMessageState: String { +public enum LocalMessageState: String, Sendable { /// The message is waiting to be synced. case pendingSync /// The message is currently being synced @@ -509,7 +509,7 @@ public enum LocalMessageState: String { } } -public enum LocalReactionState: String { +public enum LocalReactionState: String, Sendable { /// The reaction state is unknown case unknown = "" @@ -533,7 +533,7 @@ public enum LocalReactionState: String { } /// The type describing message delivery status. -public struct MessageDeliveryStatus: RawRepresentable, Hashable { +public struct MessageDeliveryStatus: RawRepresentable, Hashable, Sendable { public let rawValue: String public init(rawValue: String) { diff --git a/Sources/StreamChat/Models/CurrentUser.swift b/Sources/StreamChat/Models/CurrentUser.swift index 5c1dde2daa3..a0e9c8638a1 100644 --- a/Sources/StreamChat/Models/CurrentUser.swift +++ b/Sources/StreamChat/Models/CurrentUser.swift @@ -22,7 +22,7 @@ extension UserId { /// A type representing the currently logged-in user. `CurrentChatUser` is an immutable snapshot of a current user entity at /// the given time. /// -public class CurrentChatUser: ChatUser { +public class CurrentChatUser: ChatUser, @unchecked Sendable { /// A list of devices associcated with the user. public let devices: [Device] @@ -119,7 +119,7 @@ public class CurrentChatUser: ChatUser { } /// The total unread information from the current user. -public struct CurrentUserUnreads { +public struct CurrentUserUnreads: Sendable { /// The total number of unread messages. public let totalUnreadMessagesCount: Int /// The total number of unread channels. @@ -135,7 +135,7 @@ public struct CurrentUserUnreads { } /// The unread information of a channel. -public struct UnreadChannel { +public struct UnreadChannel: Sendable { /// The channel id. public let channelId: ChannelId /// The number of unread messages inside the channel. @@ -145,7 +145,7 @@ public struct UnreadChannel { } /// The unread information from channels with a specific type. -public struct UnreadChannelByType { +public struct UnreadChannelByType: Sendable { /// The channel type. public let channelType: ChannelType /// The number of unread channels of this channel type. @@ -155,7 +155,7 @@ public struct UnreadChannelByType { } /// The unread information of a thread. -public struct UnreadThread { +public struct UnreadThread: Sendable { /// The message id of the root of the thread. public let parentMessageId: MessageId /// The number of unread replies inside the thread. diff --git a/Sources/StreamChat/Models/Device.swift b/Sources/StreamChat/Models/Device.swift index 569329aa02d..3ca388f30aa 100644 --- a/Sources/StreamChat/Models/Device.swift +++ b/Sources/StreamChat/Models/Device.swift @@ -13,7 +13,7 @@ extension Data { } /// An object representing a device which can receive push notifications. -public struct Device: Codable, Equatable { +public struct Device: Codable, Equatable, Sendable { private enum CodingKeys: String, CodingKey { case id case createdAt = "created_at" diff --git a/Sources/StreamChat/Models/DraftMessage.swift b/Sources/StreamChat/Models/DraftMessage.swift index 8742794a357..2e636049925 100644 --- a/Sources/StreamChat/Models/DraftMessage.swift +++ b/Sources/StreamChat/Models/DraftMessage.swift @@ -4,7 +4,7 @@ import Foundation -public struct DraftMessage { +public struct DraftMessage: Sendable { /// A unique identifier of the message. public let id: MessageId @@ -41,8 +41,8 @@ public struct DraftMessage { /// Quoted message. /// /// If message is inline reply this property will contain the message quoted by this reply. - public var quotedMessage: ChatMessage? { _quotedMessage() } - let _quotedMessage: () -> ChatMessage? + public var quotedMessage: ChatMessage? { _quotedMessage?.value as? ChatMessage } + let _quotedMessage: BoxedAny? /// A list of users that are mentioned in this message. public let mentionedUsers: Set @@ -65,7 +65,7 @@ public struct DraftMessage { showReplyInChannel: Bool, extraData: [String: RawJSON], currentUser: ChatUser, - quotedMessage: @escaping () -> ChatMessage?, + quotedMessage: ChatMessage?, mentionedUsers: Set, attachments: [AnyChatMessageAttachment] ) { @@ -79,7 +79,7 @@ public struct DraftMessage { self.arguments = arguments self.showReplyInChannel = showReplyInChannel self.extraData = extraData - _quotedMessage = quotedMessage + _quotedMessage = BoxedAny(quotedMessage) self.mentionedUsers = mentionedUsers self.attachments = attachments self.currentUser = currentUser @@ -96,7 +96,7 @@ public struct DraftMessage { threadId = message.parentMessageId showReplyInChannel = message.showReplyInChannel extraData = message.extraData - _quotedMessage = { message.quotedMessage } + _quotedMessage = BoxedAny(message.quotedMessage) mentionedUsers = message.mentionedUsers attachments = message.allAttachments currentUser = message.author @@ -137,7 +137,7 @@ extension ChatMessage { showReplyInChannel = draft.showReplyInChannel replyCount = 0 extraData = draft.extraData - _quotedMessage = { draft.quotedMessage } + _quotedMessage = BoxedAny(draft.quotedMessage) isBounced = false isSilent = false isShadowed = false diff --git a/Sources/StreamChat/Models/Member.swift b/Sources/StreamChat/Models/Member.swift index 7f3c559deef..0ce06714a8d 100644 --- a/Sources/StreamChat/Models/Member.swift +++ b/Sources/StreamChat/Models/Member.swift @@ -5,7 +5,7 @@ import Foundation /// A type representing a chat channel member. `ChatChannelMember` is an immutable snapshot of a channel entity at the given time. -public class ChatChannelMember: ChatUser { +public class ChatChannelMember: ChatUser, @unchecked Sendable { /// The role of the user within the channel. public let memberRole: MemberRole @@ -164,7 +164,7 @@ public class ChatChannelMember: ChatUser { /// A `struct` describing roles of a member in a channel. /// There are some predefined types but any type can be introduced and sent by the backend. -public struct MemberRole: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral { +public struct MemberRole: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable { public let rawValue: String public init(rawValue: String) { diff --git a/Sources/StreamChat/Models/MessageModerationDetails.swift b/Sources/StreamChat/Models/MessageModerationDetails.swift index e14d6a74335..c0287cd2094 100644 --- a/Sources/StreamChat/Models/MessageModerationDetails.swift +++ b/Sources/StreamChat/Models/MessageModerationDetails.swift @@ -5,7 +5,7 @@ import Foundation /// Describes the details of a message which was moderated. -public struct MessageModerationDetails { +public struct MessageModerationDetails: Sendable { /// The original message text. public let originalText: String /// The type of moderation performed to a message. @@ -26,7 +26,7 @@ public struct MessageModerationDetails { } /// The type of moderation performed to a message. -public struct MessageModerationAction: RawRepresentable, Equatable { +public struct MessageModerationAction: RawRepresentable, Equatable, Sendable { public let rawValue: String public init(rawValue: String) { diff --git a/Sources/StreamChat/Models/MessagePinning.swift b/Sources/StreamChat/Models/MessagePinning.swift index dfe15f5cd14..9c10d21fc93 100644 --- a/Sources/StreamChat/Models/MessagePinning.swift +++ b/Sources/StreamChat/Models/MessagePinning.swift @@ -5,7 +5,7 @@ import Foundation /// Describes the pinning expiration -public struct MessagePinning: Equatable { +public struct MessagePinning: Equatable, Sendable { /// The expiration date of the pinning. Infinite expiration in case it is `nil`. public let expirationDate: Date? diff --git a/Sources/StreamChat/Models/MessageReaction.swift b/Sources/StreamChat/Models/MessageReaction.swift index fe81ae467b1..efa433ccaa3 100644 --- a/Sources/StreamChat/Models/MessageReaction.swift +++ b/Sources/StreamChat/Models/MessageReaction.swift @@ -6,7 +6,7 @@ import Foundation /// A type representing a message reaction. `ChatMessageReaction` is an immutable snapshot /// of a message reaction entity at the given time. -public struct ChatMessageReaction: Hashable { +public struct ChatMessageReaction: Hashable, Sendable { /// The id of the reaction. let id: String diff --git a/Sources/StreamChat/Models/MessageReactionGroup.swift b/Sources/StreamChat/Models/MessageReactionGroup.swift index 4dbfe5392dd..6644ac7efe6 100644 --- a/Sources/StreamChat/Models/MessageReactionGroup.swift +++ b/Sources/StreamChat/Models/MessageReactionGroup.swift @@ -5,7 +5,7 @@ import Foundation /// All the reactions information about a specific type of reaction. -public struct ChatMessageReactionGroup: Equatable { +public struct ChatMessageReactionGroup: Equatable, Sendable { /// The type of reaction. public let type: MessageReactionType /// The sum of all reaction scores for this type of reaction. diff --git a/Sources/StreamChat/Models/MessageReactionType.swift b/Sources/StreamChat/Models/MessageReactionType.swift index 8d7dc6019a3..07179e69d9e 100644 --- a/Sources/StreamChat/Models/MessageReactionType.swift +++ b/Sources/StreamChat/Models/MessageReactionType.swift @@ -10,7 +10,7 @@ import Foundation /// will be displayed in the application. /// /// Common examples are: "like", "love", "smile", etc. -public struct MessageReactionType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral { +public struct MessageReactionType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable { // MARK: - RawRepresentable public let rawValue: String diff --git a/Sources/StreamChat/Models/MuteDetails.swift b/Sources/StreamChat/Models/MuteDetails.swift index 06ae3f8f183..1c535c3c235 100644 --- a/Sources/StreamChat/Models/MuteDetails.swift +++ b/Sources/StreamChat/Models/MuteDetails.swift @@ -5,7 +5,7 @@ import Foundation /// Describes user/channel mute details. -public struct MuteDetails: Equatable { +public struct MuteDetails: Equatable, Sendable { /// The time when the mute action was taken. public let createdAt: Date /// The time when the mute was updated. diff --git a/Sources/StreamChat/Models/Poll.swift b/Sources/StreamChat/Models/Poll.swift index 6ecb5328329..970f9cd13bc 100644 --- a/Sources/StreamChat/Models/Poll.swift +++ b/Sources/StreamChat/Models/Poll.swift @@ -5,7 +5,7 @@ import Foundation /// The model for a Poll. -public struct Poll: Equatable { +public struct Poll: Equatable, Sendable { /// A boolean indicating whether the poll allows answers/comments. public let allowAnswers: Bool diff --git a/Sources/StreamChat/Models/PollOption.swift b/Sources/StreamChat/Models/PollOption.swift index 2e7d721db7f..70d3d23fb16 100644 --- a/Sources/StreamChat/Models/PollOption.swift +++ b/Sources/StreamChat/Models/PollOption.swift @@ -5,7 +5,7 @@ import Foundation /// The model for an option in a poll. -public struct PollOption: Hashable, Equatable { +public struct PollOption: Hashable, Equatable, Sendable { /// The unique identifier of the poll option. public let id: String diff --git a/Sources/StreamChat/Models/PollVote.swift b/Sources/StreamChat/Models/PollVote.swift index 233fe2c6eda..6cdee431e8d 100644 --- a/Sources/StreamChat/Models/PollVote.swift +++ b/Sources/StreamChat/Models/PollVote.swift @@ -5,7 +5,7 @@ import Foundation /// A structure representing a vote in a poll. -public struct PollVote: Hashable, Equatable { +public struct PollVote: Hashable, Equatable, Sendable { /// The unique identifier of the poll vote. public let id: String diff --git a/Sources/StreamChat/Models/PushProvider.swift b/Sources/StreamChat/Models/PushProvider.swift index 8804d6ee8d6..07ce65b63b5 100644 --- a/Sources/StreamChat/Models/PushProvider.swift +++ b/Sources/StreamChat/Models/PushProvider.swift @@ -5,7 +5,7 @@ import Foundation /// A type that represents the supported push providers. -public struct PushProvider: RawRepresentable, Hashable, ExpressibleByStringLiteral { +public struct PushProvider: RawRepresentable, Hashable, ExpressibleByStringLiteral, Sendable { public static let firebase: Self = "firebase" public static let apn: Self = "apn" diff --git a/Sources/StreamChat/Models/Thread.swift b/Sources/StreamChat/Models/Thread.swift index 55a4d4ef507..1058100a952 100644 --- a/Sources/StreamChat/Models/Thread.swift +++ b/Sources/StreamChat/Models/Thread.swift @@ -5,7 +5,7 @@ import Foundation /// A type representing a thread. -public struct ChatThread { +public struct ChatThread: Sendable { /// The id of the message which created the thread. It is also the id of the thread. public let parentMessageId: MessageId /// The parent message which is the root of this thread. diff --git a/Sources/StreamChat/Models/ThreadParticipant.swift b/Sources/StreamChat/Models/ThreadParticipant.swift index 6cc2f9f0cff..4516edaf11e 100644 --- a/Sources/StreamChat/Models/ThreadParticipant.swift +++ b/Sources/StreamChat/Models/ThreadParticipant.swift @@ -5,7 +5,7 @@ import Foundation /// The details of a participant in a thread. -public struct ThreadParticipant: Equatable { +public struct ThreadParticipant: Equatable, Sendable { /// The user information of the participant. public let user: ChatUser /// The id of the thread, which is also the id of the parent message. diff --git a/Sources/StreamChat/Models/ThreadRead.swift b/Sources/StreamChat/Models/ThreadRead.swift index 1995ea8daf8..43e5bb6eeae 100644 --- a/Sources/StreamChat/Models/ThreadRead.swift +++ b/Sources/StreamChat/Models/ThreadRead.swift @@ -5,7 +5,7 @@ import Foundation /// The information about a thread read. -public struct ThreadRead: Equatable { +public struct ThreadRead: Equatable, Sendable { /// The user which the read belongs to. public let user: ChatUser /// The date when the user last read the thread. diff --git a/Sources/StreamChat/Models/UnreadCount.swift b/Sources/StreamChat/Models/UnreadCount.swift index d0e03eeb68b..121ff8f4cc2 100644 --- a/Sources/StreamChat/Models/UnreadCount.swift +++ b/Sources/StreamChat/Models/UnreadCount.swift @@ -5,7 +5,7 @@ import Foundation /// A struct containing information about unread counts of channels and messages. -public struct UnreadCount: Decodable, Equatable { +public struct UnreadCount: Decodable, Equatable, Sendable { /// The default value representing no unread channels, messages and threads. public static let noUnread = UnreadCount(channels: 0, messages: 0, threads: 0) diff --git a/Sources/StreamChat/Models/User.swift b/Sources/StreamChat/Models/User.swift index a71a3dd92e4..cfc8adfa403 100644 --- a/Sources/StreamChat/Models/User.swift +++ b/Sources/StreamChat/Models/User.swift @@ -12,7 +12,7 @@ public typealias TeamId = String /// A type representing a chat user. `ChatUser` is an immutable snapshot of a chat user entity at the given time. /// -public class ChatUser { +public class ChatUser: @unchecked Sendable { /// The unique identifier of the user. public let id: UserId @@ -128,7 +128,7 @@ extension ChatUser: Equatable { } } -public struct UserRole: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral { +public struct UserRole: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable { public let rawValue: String public init(rawValue: String) { diff --git a/Sources/StreamChat/Models/UserInfo.swift b/Sources/StreamChat/Models/UserInfo.swift index a108ef0327a..000c9da7631 100644 --- a/Sources/StreamChat/Models/UserInfo.swift +++ b/Sources/StreamChat/Models/UserInfo.swift @@ -5,7 +5,7 @@ import Foundation /// A model containing user info that's used to connect to chat's backend -public struct UserInfo { +public struct UserInfo: Sendable { /// The id of the user. public let id: UserId /// The name of the user. @@ -45,7 +45,7 @@ public struct UserInfo { } /// The privacy settings of the user. -public struct UserPrivacySettings { +public struct UserPrivacySettings: Sendable { /// The settings for typing indicator events. public var typingIndicators: TypingIndicatorPrivacySettings? /// The settings for the read receipt events. @@ -61,7 +61,7 @@ public struct UserPrivacySettings { } /// The settings for typing indicator events. -public struct TypingIndicatorPrivacySettings { +public struct TypingIndicatorPrivacySettings: Sendable { public var enabled: Bool public init(enabled: Bool = true) { @@ -70,7 +70,7 @@ public struct TypingIndicatorPrivacySettings { } /// The settings for the read receipt events. -public struct ReadReceiptsPrivacySettings { +public struct ReadReceiptsPrivacySettings: Sendable { public var enabled: Bool public init(enabled: Bool = true) { diff --git a/Sources/StreamChat/Query/ChannelListQuery.swift b/Sources/StreamChat/Query/ChannelListQuery.swift index ff4f35cf76b..4436b5a509f 100644 --- a/Sources/StreamChat/Query/ChannelListQuery.swift +++ b/Sources/StreamChat/Query/ChannelListQuery.swift @@ -233,7 +233,7 @@ internal extension FilterKey where Scope: AnyChannelListFilterScope { /// A query is used for querying specific channels from backend. /// You can specify filter, sorting, pagination, limit for fetched messages in channel and other options. -public struct ChannelListQuery: Encodable { +public struct ChannelListQuery: Encodable, Sendable { private enum CodingKeys: String, CodingKey { case filter = "filter_conditions" case sort diff --git a/Sources/StreamChat/Query/ChannelMemberListQuery.swift b/Sources/StreamChat/Query/ChannelMemberListQuery.swift index 7cb79b2c534..ab8b896ab20 100644 --- a/Sources/StreamChat/Query/ChannelMemberListQuery.swift +++ b/Sources/StreamChat/Query/ChannelMemberListQuery.swift @@ -59,7 +59,7 @@ public extension FilterKey where Scope: AnyMemberListFilterScope { } /// A query type used for fetching channel members from the backend. -public struct ChannelMemberListQuery: Encodable { +public struct ChannelMemberListQuery: Encodable, Sendable { private enum CodingKeys: String, CodingKey { case filter = "filter_conditions" case sort @@ -121,6 +121,14 @@ extension ChannelMemberListQuery { } } +extension ChannelMemberListQuery { + func withPagination(_ pagination: Pagination) -> Self { + var query = self + query.pagination = pagination + return query + } +} + extension ChannelMemberListQuery { /// Builds `ChannelMemberListQuery` for a single member in a specific channel /// - Parameters: diff --git a/Sources/StreamChat/Query/ChannelQuery.swift b/Sources/StreamChat/Query/ChannelQuery.swift index 579086d0c43..ea6e6df15b5 100644 --- a/Sources/StreamChat/Query/ChannelQuery.swift +++ b/Sources/StreamChat/Query/ChannelQuery.swift @@ -5,7 +5,7 @@ import Foundation /// A channel query. -public struct ChannelQuery: Encodable { +public struct ChannelQuery: Encodable, Sendable { enum CodingKeys: String, CodingKey { case data case messages diff --git a/Sources/StreamChat/Query/ChannelWatcherListQuery.swift b/Sources/StreamChat/Query/ChannelWatcherListQuery.swift index 260ccb871a7..6ceed4e3e23 100644 --- a/Sources/StreamChat/Query/ChannelWatcherListQuery.swift +++ b/Sources/StreamChat/Query/ChannelWatcherListQuery.swift @@ -8,7 +8,7 @@ import Foundation /// /// Learn more about watchers in our documentation [here](https://getstream.io/chat/docs/ios/watch_channel/?language=swift) /// -public struct ChannelWatcherListQuery: Encodable { +public struct ChannelWatcherListQuery: Encodable, Sendable { private enum CodingKeys: String, CodingKey { case watchers } diff --git a/Sources/StreamChat/Query/DraftListQuery.swift b/Sources/StreamChat/Query/DraftListQuery.swift index 1c2367f219f..5a4bf9a0a4a 100644 --- a/Sources/StreamChat/Query/DraftListQuery.swift +++ b/Sources/StreamChat/Query/DraftListQuery.swift @@ -5,7 +5,7 @@ import Foundation /// A query used to fetch the drafts of the current user. -public struct DraftListQuery: Encodable { +public struct DraftListQuery: Encodable, Sendable { /// The pagination information to query the votes. public var pagination: Pagination /// The sorting parameter. By default drafts are sorted by newest first. diff --git a/Sources/StreamChat/Query/Filter.swift b/Sources/StreamChat/Query/Filter.swift index aa3a633a661..067195ee0bb 100644 --- a/Sources/StreamChat/Query/Filter.swift +++ b/Sources/StreamChat/Query/Filter.swift @@ -63,7 +63,7 @@ public protocol FilterScope {} /// /// Only types representing text, numbers, booleans, dates, and other filters can be on the "right-hand" side of `Filter`. /// -public protocol FilterValue: Encodable {} +public protocol FilterValue: Encodable, Sendable {} // Built-in `FilterValue` conformances for supported types @@ -89,7 +89,7 @@ extension Optional: FilterValue where Wrapped == TeamId {} /// /// Learn more about how to create simple, advanced, and custom filters in our [cheat sheet](https://github.com/GetStream/stream-chat-swift/wiki/StreamChat-SDK-Cheat-Sheet#query-filters). /// -public struct Filter { +public struct Filter: Sendable { /// An operator used for the filter. public let `operator`: String @@ -102,7 +102,7 @@ public struct Filter { /// The mapper that will transform the input value to a value that /// can be compared with the DB value - typealias ValueMapper = (Any) -> FilterValue? + typealias ValueMapper = @Sendable(Any) -> FilterValue? let valueMapper: ValueMapper? /// The keypath of the DB object that will be compared with the input value during @@ -114,7 +114,7 @@ public struct Filter { /// The mapper that will override the DB Predicate. This might be needed /// for cases where our DB value is completely different from the server value. - typealias PredicateMapper = (FilterOperator, Any) -> NSPredicate? + typealias PredicateMapper = @Sendable(FilterOperator, Any) -> NSPredicate? let predicateMapper: PredicateMapper? init( @@ -224,7 +224,7 @@ public extension Filter { /// /// Learn more about how to create filter keys for your custom extra data in our [cheat sheet](https://github.com/GetStream/stream-chat-swift/wiki/StreamChat-SDK-Cheat-Sheet#query-filters). /// -public struct FilterKey: ExpressibleByStringLiteral, RawRepresentable { +public struct FilterKey: ExpressibleByStringLiteral, RawRepresentable, Sendable { /// The raw value of the key. This value should match the "encodable" key for the given object. public let rawValue: String @@ -234,14 +234,14 @@ public struct FilterKey: ExpressibleBySt /// The mapper that will transform the input value to a value that /// can be compared with the DB value - typealias ValueMapper = (Any) -> FilterValue? - typealias TypedValueMapper = (Value) -> FilterValue? + typealias ValueMapper = @Sendable(Any) -> FilterValue? + typealias TypedValueMapper = @Sendable(Value) -> FilterValue? let valueMapper: ValueMapper? /// The mapper that will override the DB Predicate. This might be needed /// for cases where our DB value is completely different from the server value. - typealias PredicateMapper = (FilterOperator, Any) -> NSPredicate? - typealias TypedPredicateMapper = (FilterOperator, Value) -> NSPredicate? + typealias PredicateMapper = @Sendable(FilterOperator, Any) -> NSPredicate? + typealias TypedPredicateMapper = @Sendable(FilterOperator, Value) -> NSPredicate? let predicateMapper: PredicateMapper? let isCollectionFilter: Bool diff --git a/Sources/StreamChat/Query/MessageSearchQuery.swift b/Sources/StreamChat/Query/MessageSearchQuery.swift index 2d2b6d1cf7a..ff4ec4ceb50 100644 --- a/Sources/StreamChat/Query/MessageSearchQuery.swift +++ b/Sources/StreamChat/Query/MessageSearchQuery.swift @@ -43,9 +43,9 @@ public enum MessageSearchSortingKey: String, SortingKey { } /// Default sort descriptor for Message search. Corresponds to `created_at` - static let defaultSortDescriptor: NSSortDescriptor = { + static var defaultSortDescriptor: NSSortDescriptor { NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: true) - }() + } func sortDescriptor(isAscending: Bool) -> NSSortDescriptor? { canUseAsSortDescriptor ? .init(key: rawValue, ascending: isAscending) : nil @@ -83,7 +83,7 @@ public extension Filter where Scope: AnyMessageSearchFilterScope { } } -public struct MessageSearchQuery: Encodable { +public struct MessageSearchQuery: Encodable, Sendable { private enum CodingKeys: String, CodingKey { case query case channelFilter = "filter_conditions" diff --git a/Sources/StreamChat/Query/Pagination.swift b/Sources/StreamChat/Query/Pagination.swift index d36ee1e1052..12e6c3a65ec 100644 --- a/Sources/StreamChat/Query/Pagination.swift +++ b/Sources/StreamChat/Query/Pagination.swift @@ -19,7 +19,7 @@ public extension Int { /// Basic pagination with `pageSize` and `offset`. /// Used everywhere except `ChannelQuery`. (See `MessagesPagination`) -public struct Pagination: Encodable, Equatable { +public struct Pagination: Encodable, Equatable, Sendable { /// A page size. public let pageSize: Int /// An offset. @@ -56,7 +56,7 @@ public struct Pagination: Encodable, Equatable { } } -public struct MessagesPagination: Encodable, Equatable { +public struct MessagesPagination: Encodable, Equatable, Sendable { /// A page size public let pageSize: Int /// Parameter for pagination. @@ -79,7 +79,7 @@ public struct MessagesPagination: Encodable, Equatable { } /// Pagination parameters -public enum PaginationParameter: Encodable, Hashable { +public enum PaginationParameter: Encodable, Hashable, Sendable { enum CodingKeys: String, CodingKey { case greaterThan = "id_gt" case greaterThanOrEqual = "id_gte" diff --git a/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesPagination.swift b/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesPagination.swift index 005fbe5c6ee..5e221ca6533 100644 --- a/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesPagination.swift +++ b/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesPagination.swift @@ -5,7 +5,7 @@ import Foundation /// Pagination options available when paginating pinned messages. -public enum PinnedMessagesPagination: Hashable { +public enum PinnedMessagesPagination: Hashable, Sendable { /// When used, the backend returns messages around the message with the given id. case aroundMessage(_ messageId: MessageId) diff --git a/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesQuery.swift b/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesQuery.swift index 14b1e610031..98c4e8624ba 100644 --- a/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesQuery.swift +++ b/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesQuery.swift @@ -5,7 +5,7 @@ import Foundation /// The query used to paginate pinned messages. -struct PinnedMessagesQuery: Hashable { +struct PinnedMessagesQuery: Hashable, Sendable { /// The page size. let pageSize: Int diff --git a/Sources/StreamChat/Query/PollVoteListQuery.swift b/Sources/StreamChat/Query/PollVoteListQuery.swift index 7f7d75c0962..61d280a1b97 100644 --- a/Sources/StreamChat/Query/PollVoteListQuery.swift +++ b/Sources/StreamChat/Query/PollVoteListQuery.swift @@ -5,7 +5,7 @@ import Foundation /// A query used for querying specific votes from a poll. -public struct PollVoteListQuery: Encodable { +public struct PollVoteListQuery: Encodable, Sendable { /// The pollId which the votes belong to. public var pollId: String /// The optionId which the votes belong to in case the query relates to only one poll option. diff --git a/Sources/StreamChat/Query/QueryOptions.swift b/Sources/StreamChat/Query/QueryOptions.swift index bdf6c756220..2d6b6405fee 100644 --- a/Sources/StreamChat/Query/QueryOptions.swift +++ b/Sources/StreamChat/Query/QueryOptions.swift @@ -5,7 +5,7 @@ import Foundation /// Query options. -public struct QueryOptions: OptionSet, Encodable { +public struct QueryOptions: OptionSet, Encodable, Sendable { private enum CodingKeys: String, CodingKey { case state case watch diff --git a/Sources/StreamChat/Query/ReactionListQuery.swift b/Sources/StreamChat/Query/ReactionListQuery.swift index 609114d0735..71472574a1a 100644 --- a/Sources/StreamChat/Query/ReactionListQuery.swift +++ b/Sources/StreamChat/Query/ReactionListQuery.swift @@ -5,7 +5,7 @@ import Foundation /// A query used for querying specific reactions from a message. -public struct ReactionListQuery: Encodable { +public struct ReactionListQuery: Encodable, Sendable { /// The message id that the reactions belong to. public var messageId: MessageId /// The pagination information to query the reactions. diff --git a/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift b/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift index 5b47b11f066..696c1678247 100644 --- a/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift +++ b/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift @@ -104,10 +104,10 @@ extension ChannelListSortingKey: CustomDebugStringConvertible { } extension ChannelListSortingKey { - static let defaultSortDescriptor: NSSortDescriptor = { + static var defaultSortDescriptor: NSSortDescriptor { let dateKeyPath: KeyPath = \ChannelDTO.defaultSortingAt return .init(keyPath: dateKeyPath, ascending: false) - }() + } func sortDescriptor(isAscending: Bool) -> NSSortDescriptor? { guard let localKey = self.localKey else { @@ -142,3 +142,11 @@ extension ChatChannel { lastMessageAt ?? createdAt } } + +// Remove when InferSendableFromCaptures is enabled +// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0418-inferring-sendable-for-methods.md +#if compiler(>=6.0) +extension PartialKeyPath: @retroactive @unchecked Sendable where Root == ChatChannel {} +#else +extension PartialKeyPath: @unchecked Sendable where Root == ChatChannel {} +#endif diff --git a/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift b/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift index f580bf43330..1699305cf83 100644 --- a/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift +++ b/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift @@ -36,10 +36,10 @@ public enum ChannelMemberListSortingKey: String, SortingKey { } extension ChannelMemberListSortingKey { - static let defaultSortDescriptor: NSSortDescriptor = { + static var defaultSortDescriptor: NSSortDescriptor { let dateKeyPath: KeyPath = \MemberDTO.memberCreatedAt return .init(keyPath: dateKeyPath, ascending: false) - }() + } func sortDescriptor(isAscending: Bool) -> NSSortDescriptor { .init(key: rawValue, ascending: isAscending) diff --git a/Sources/StreamChat/Query/Sorting/Sorting.swift b/Sources/StreamChat/Query/Sorting/Sorting.swift index 1682986d0f0..c9cc2eef291 100644 --- a/Sources/StreamChat/Query/Sorting/Sorting.swift +++ b/Sources/StreamChat/Query/Sorting/Sorting.swift @@ -5,7 +5,7 @@ import Foundation /// A sorting key protocol. -public protocol SortingKey: Encodable {} +public protocol SortingKey: Encodable, Sendable {} /// Sorting options. /// @@ -14,7 +14,7 @@ public protocol SortingKey: Encodable {} /// // Sort channels by the last message date: /// let sorting = Sorting("lastMessageDate") /// ``` -public struct Sorting: Encodable, CustomStringConvertible { +public struct Sorting: Encodable, CustomStringConvertible, Sendable { /// A sorting field name. public let key: Key /// A sorting direction. diff --git a/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift b/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift index de41f50441a..f88e01065d0 100644 --- a/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift +++ b/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift @@ -34,10 +34,10 @@ public enum UserListSortingKey: String, SortingKey { } extension UserListSortingKey { - static let defaultSortDescriptor: NSSortDescriptor = { + static var defaultSortDescriptor: NSSortDescriptor { let stringKeyPath: KeyPath = \UserDTO.id return .init(keyPath: stringKeyPath, ascending: false) - }() + } func sortDescriptor(isAscending: Bool) -> NSSortDescriptor? { .init(key: rawValue, ascending: isAscending) diff --git a/Sources/StreamChat/Query/ThreadListQuery.swift b/Sources/StreamChat/Query/ThreadListQuery.swift index 5563469cea5..56e800d0354 100644 --- a/Sources/StreamChat/Query/ThreadListQuery.swift +++ b/Sources/StreamChat/Query/ThreadListQuery.swift @@ -5,7 +5,7 @@ import Foundation /// A query to fetch the list of threads the current belongs to. -public struct ThreadListQuery: Encodable { +public struct ThreadListQuery: Encodable, Sendable { private enum CodingKeys: String, CodingKey { case watch case replyLimit = "reply_limit" diff --git a/Sources/StreamChat/Query/ThreadQuery.swift b/Sources/StreamChat/Query/ThreadQuery.swift index 4ecaa08ca38..f6f08d1d003 100644 --- a/Sources/StreamChat/Query/ThreadQuery.swift +++ b/Sources/StreamChat/Query/ThreadQuery.swift @@ -6,7 +6,7 @@ import Foundation /// A query to fetch information about a thread. /// To fetch all the replies from a thread and paginate the replies, the `ChatMessageController` should be used instead. -public struct ThreadQuery: Encodable { +public struct ThreadQuery: Encodable, Sendable { private enum CodingKeys: String, CodingKey { case messageId = "message_id" case watch diff --git a/Sources/StreamChat/Query/UserListQuery.swift b/Sources/StreamChat/Query/UserListQuery.swift index 210aa1481bd..71e1877036a 100644 --- a/Sources/StreamChat/Query/UserListQuery.swift +++ b/Sources/StreamChat/Query/UserListQuery.swift @@ -58,7 +58,7 @@ public extension FilterKey where Scope: AnyUserListFilterScope { /// A query is used for querying specific users from backend. /// You can specify filter, sorting and pagination. -public struct UserListQuery: Encodable { +public struct UserListQuery: Encodable, Sendable { private enum CodingKeys: String, CodingKey { case filter = "filter_conditions" case sort diff --git a/Sources/StreamChat/Repositories/AuthenticationRepository.swift b/Sources/StreamChat/Repositories/AuthenticationRepository.swift index c4d323adcce..a200ddae7bd 100644 --- a/Sources/StreamChat/Repositories/AuthenticationRepository.swift +++ b/Sources/StreamChat/Repositories/AuthenticationRepository.swift @@ -4,7 +4,7 @@ import Foundation -public typealias TokenProvider = (@escaping (Result) -> Void) -> Void +public typealias TokenProvider = @Sendable(@escaping @Sendable(Result) -> Void) -> Void enum EnvironmentState { case firstConnection @@ -27,7 +27,7 @@ protocol AuthenticationRepositoryDelegate: AnyObject { func logOutUser(completion: @escaping () -> Void) } -class AuthenticationRepository { +class AuthenticationRepository: @unchecked Sendable { private enum Constants { /// Maximum amount of consecutive token refresh attempts before failing static let maximumTokenRefreshAttempts = 10 @@ -46,8 +46,8 @@ class AuthenticationRepository { private var _currentToken: Token? private var _tokenExpirationRetryStrategy: RetryStrategy private var _tokenProvider: TokenProvider? - private var _tokenRequestCompletions: [(Error?) -> Void] = [] - private var _tokenWaiters: [String: (Result) -> Void] = [:] + private var _tokenRequestCompletions: [@Sendable(Error?) -> Void] = [] + private var _tokenWaiters: [String: @Sendable(Result) -> Void] = [:] private var _tokenProviderTimer: TimerControl? private var _connectionProviderTimer: TimerControl? @@ -142,7 +142,7 @@ class AuthenticationRepository { /// - Parameters: /// - userInfo: The user information that will be created OR updated if it exists. /// - tokenProvider: The block to be used to get a token. - func connectUser(userInfo: UserInfo, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { + func connectUser(userInfo: UserInfo, tokenProvider: @escaping TokenProvider, completion: @escaping @Sendable(Error?) -> Void) { var logOutFirst: Bool { if let currentUserId = currentUserId, currentUserId.isGuest { return true @@ -158,7 +158,7 @@ class AuthenticationRepository { /// Establishes a connection for a guest user. /// - Parameters: /// - userInfo: The user information that will be created OR updated if it exists. - func connectGuestUser(userInfo: UserInfo, completion: @escaping (Error?) -> Void) { + func connectGuestUser(userInfo: UserInfo, completion: @escaping @Sendable(Error?) -> Void) { let tokenProvider: TokenProvider = { [weak self] completion in self?.fetchGuestToken(userInfo: userInfo, completion: completion) } @@ -166,12 +166,12 @@ class AuthenticationRepository { } /// Establishes a connection for an anonymous user. - func connectAnonymousUser(completion: @escaping (Error?) -> Void) { + func connectAnonymousUser(completion: @escaping @Sendable(Error?) -> Void) { let tokenProvider: TokenProvider = { $0(.success(.anonymous)) } executeTokenFetch(logOutFirst: true, userInfo: nil, tokenProvider: tokenProvider, completion: completion) } - private func executeTokenFetch(logOutFirst: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { + private func executeTokenFetch(logOutFirst: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping @Sendable(Error?) -> Void) { log.assert(delegate != nil, "Delegate should not be nil at this point") let handleTokenFetch = { [weak self] in @@ -211,7 +211,7 @@ class AuthenticationRepository { currentUserId = nil } - func refreshToken(completion: @escaping (Error?) -> Void) { + func refreshToken(completion: @escaping @Sendable(Error?) -> Void) { guard let tokenProvider = tokenProvider else { let error = ClientError.MissingTokenProvider() log.assertionFailure(error.localizedDescription) @@ -251,7 +251,7 @@ class AuthenticationRepository { } } - func provideToken(timeout: TimeInterval = 10, completion: @escaping (Result) -> Void) { + func provideToken(timeout: TimeInterval = 10, completion: @escaping @Sendable(Result) -> Void) { if let token = currentToken { completion(.success(token)) return @@ -284,7 +284,7 @@ class AuthenticationRepository { } func completeTokenCompletions(error: Error?) { - let completionBlocks: [(Error?) -> Void]? = tokenQueue.sync(flags: .barrier) { + let completionBlocks: [@Sendable(Error?) -> Void]? = tokenQueue.sync(flags: .barrier) { self._isGettingToken = false let completions = self._tokenRequestCompletions return completions @@ -297,7 +297,7 @@ class AuthenticationRepository { } private func updateToken(token: Token?, notifyTokenWaiters: Bool) { - let waiters: [String: (Result) -> Void] = tokenQueue.sync(flags: .barrier) { + let waiters: [String: @Sendable(Result) -> Void] = tokenQueue.sync(flags: .barrier) { _currentToken = token _currentUserId = token?.userId guard notifyTokenWaiters else { return [:] } @@ -315,7 +315,7 @@ class AuthenticationRepository { } } - private func scheduleTokenFetch(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { + private func scheduleTokenFetch(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping @Sendable(Error?) -> Void) { guard !isGettingToken || isRetry else { tokenQueue.async(flags: .barrier) { self._tokenRequestCompletions.append(completion) @@ -335,7 +335,7 @@ class AuthenticationRepository { } } - private func getToken(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { + private func getToken(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping @Sendable(Error?) -> Void) { tokenQueue.async(flags: .barrier) { self._tokenRequestCompletions.append(completion) } @@ -346,7 +346,7 @@ class AuthenticationRepository { isGettingToken = true - let onCompletion: (Error?) -> Void = { [weak self] error in + let onCompletion: @Sendable(Error?) -> Void = { [weak self] error in if let error = error { log.error("Error when getting token: \(error)", subsystems: .authentication) } else { @@ -360,7 +360,7 @@ class AuthenticationRepository { return } - let onTokenReceived: (Token) -> Void = { [weak self, weak connectionRepository] token in + let onTokenReceived: @Sendable(Token) -> Void = { [weak self, weak connectionRepository] token in self?.isGettingToken = false self?.prepareEnvironment(userInfo: userInfo, newToken: token) // We manually change the `connectionStatus` for passive client @@ -370,7 +370,7 @@ class AuthenticationRepository { connectionRepository?.connect(completion: onCompletion) } - let retryFetchIfPossible: (Error?) -> Void = { [weak self] error in + let retryFetchIfPossible: @Sendable(Error?) -> Void = { [weak self] error in guard let self = self else { return } self.tokenQueue.async(flags: .barrier) { self._consecutiveRefreshFailures += 1 @@ -401,7 +401,7 @@ class AuthenticationRepository { private func fetchGuestToken( userInfo: UserInfo, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { let endpoint: Endpoint = .guestUserToken( userId: userInfo.id, @@ -427,7 +427,7 @@ class AuthenticationRepository { } extension ClientError { - public final class TooManyFailedTokenRefreshAttempts: ClientError { + public final class TooManyFailedTokenRefreshAttempts: ClientError, @unchecked Sendable { override public var localizedDescription: String { """ Token fetch has failed more than 10 times. diff --git a/Sources/StreamChat/Repositories/ChannelRepository.swift b/Sources/StreamChat/Repositories/ChannelRepository.swift index b46561c8ab4..14fe7f16fe2 100644 --- a/Sources/StreamChat/Repositories/ChannelRepository.swift +++ b/Sources/StreamChat/Repositories/ChannelRepository.swift @@ -5,7 +5,7 @@ import CoreData import Foundation -class ChannelRepository { +class ChannelRepository: @unchecked Sendable { let database: DatabaseContainer let apiClient: APIClient @@ -14,7 +14,7 @@ class ChannelRepository { self.apiClient = apiClient } - func getChannel(for query: ChannelQuery, store: Bool, completion: @escaping (Result) -> Void) { + func getChannel(for query: ChannelQuery, store: Bool, completion: @escaping @Sendable(Result) -> Void) { let endpoint: Endpoint = query.options == .state ? .channelState(query: query) : .updateChannel(query: query) apiClient.request(endpoint: endpoint) { [database] result in switch result { @@ -42,7 +42,7 @@ class ChannelRepository { func markRead( cid: ChannelId, userId: UserId, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .markRead(cid: cid)) { [weak self] result in if let error = result.error { @@ -71,7 +71,7 @@ class ChannelRepository { userId: UserId, from messageId: MessageId, lastReadMessageId: MessageId?, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { apiClient.request( endpoint: .markUnread(cid: cid, messageId: messageId, userId: userId) @@ -81,8 +81,7 @@ class ChannelRepository { return } - var channel: ChatChannel? - self?.database.write({ session in + self?.database.write(converting: { session in session.markChannelAsUnread( for: cid, userId: userId, @@ -91,13 +90,12 @@ class ChannelRepository { lastReadAt: nil, unreadMessagesCount: nil ) - channel = try session.channel(cid: cid)?.asModel() - }, completion: { error in - if let channel = channel, error == nil { - completion?(.success(channel)) - } else { - completion?(.failure(error ?? ClientError.ChannelNotCreatedYet())) + guard let channelDTO = session.channel(cid: cid) else { + throw ClientError.ChannelDoesNotExist(cid: cid) } + return try channelDTO.asModel() + }, completion: { + completion?($0) }) } } diff --git a/Sources/StreamChat/Repositories/ConnectionRepository.swift b/Sources/StreamChat/Repositories/ConnectionRepository.swift index f675e0285cb..3a4ea5a9c7a 100644 --- a/Sources/StreamChat/Repositories/ConnectionRepository.swift +++ b/Sources/StreamChat/Repositories/ConnectionRepository.swift @@ -4,9 +4,9 @@ import Foundation -class ConnectionRepository { +class ConnectionRepository: @unchecked Sendable { private let connectionQueue: DispatchQueue = DispatchQueue(label: "io.getstream.connection-repository", attributes: .concurrent) - private var _connectionIdWaiters: [String: (Result) -> Void] = [:] + private var _connectionIdWaiters: [String: @Sendable(Result) -> Void] = [:] private var _connectionId: ConnectionId? private var _connectionStatus: ConnectionStatus = .initialized @@ -53,7 +53,7 @@ class ConnectionRepository { /// - Parameters: /// - completion: Called when the connection is established. If the connection fails, the completion is called with an error. /// - func connect(completion: ((Error?) -> Void)? = nil) { + func connect(completion: (@Sendable(Error?) -> Void)? = nil) { // Connecting is not possible in connectionless mode (duh) guard isClientInActiveMode else { completion?(ClientError.ClientIsNotInActiveMode()) @@ -87,7 +87,7 @@ class ConnectionRepository { /// are received. func disconnect( source: WebSocketConnectionState.DisconnectionSource, - completion: @escaping () -> Void + completion: @escaping @Sendable() -> Void ) { apiClient.flushRequestsQueue() syncRepository.cancelRecoveryFlow() @@ -157,7 +157,7 @@ class ConnectionRepository { ) } - func provideConnectionId(timeout: TimeInterval = 10, completion: @escaping (Result) -> Void) { + func provideConnectionId(timeout: TimeInterval = 10, completion: @escaping @Sendable(Result) -> Void) { if let connectionId = connectionId { completion(.success(connectionId)) return @@ -210,7 +210,7 @@ class ConnectionRepository { connectionId: String?, shouldNotifyWaiters: Bool ) { - let waiters: [String: (Result) -> Void] = connectionQueue.sync(flags: .barrier) { + let waiters: [String: @Sendable(Result) -> Void] = connectionQueue.sync(flags: .barrier) { _connectionId = connectionId guard shouldNotifyWaiters else { return [:] } let waiters = _connectionIdWaiters diff --git a/Sources/StreamChat/Repositories/DraftMessagesRepository.swift b/Sources/StreamChat/Repositories/DraftMessagesRepository.swift index a54bdf08f79..dd20f787f32 100644 --- a/Sources/StreamChat/Repositories/DraftMessagesRepository.swift +++ b/Sources/StreamChat/Repositories/DraftMessagesRepository.swift @@ -4,12 +4,12 @@ import CoreData -struct DraftListResponse { +struct DraftListResponse: Sendable { var drafts: [DraftMessage] var next: String? } -class DraftMessagesRepository { +class DraftMessagesRepository: @unchecked Sendable { private let database: DatabaseContainer private let apiClient: APIClient @@ -20,14 +20,13 @@ class DraftMessagesRepository { func loadDrafts( query: DraftListQuery, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { apiClient.request(endpoint: .drafts(query: query)) { [weak self] result in switch result { case .success(let response): - var drafts: [DraftMessage] = [] - self?.database.write({ session in - drafts = try response.drafts.compactMap { + self?.database.write(converting: { session in + let drafts: [DraftMessage] = try response.drafts.compactMap { guard let channelId = $0.channelPayload?.cid else { return nil } @@ -35,13 +34,8 @@ class DraftMessagesRepository { .saveDraftMessage(payload: $0, for: channelId, cache: nil) .asModel()) } - }, completion: { error in - if let error { - completion(.failure(error)) - return - } - completion(.success(DraftListResponse(drafts: drafts, next: response.next))) - }) + return DraftListResponse(drafts: drafts, next: response.next) + }, completion: completion) case .failure(let error): completion(.failure(error)) } @@ -60,10 +54,9 @@ class DraftMessagesRepository { mentionedUserIds: [UserId], quotedMessageId: MessageId?, extraData: [String: RawJSON], - completion: ((Result) -> Void)? + completion: (@Sendable(Result) -> Void)? ) { - var draftRequestBody: DraftMessageRequestBody? - database.write({ (session) in + database.write(converting: { (session) in let newMessageDTO = try session.createNewDraftMessage( in: cid, text: text, @@ -77,36 +70,32 @@ class DraftMessagesRepository { quotedMessageId: quotedMessageId, extraData: extraData ) - draftRequestBody = newMessageDTO.asDraftRequestBody() - }) { error in - guard let requestBody = draftRequestBody, error == nil else { - completion?(.failure(error ?? ClientError.Unknown())) - return - } - - self.apiClient.request( - endpoint: .updateDraftMessage(channelId: cid, requestBody: requestBody) - ) { [weak self] result in - switch result { - case .success(let response): - var draft: ChatMessage? - self?.database.write({ session in - let draftPayload = response.draft - let messageDTO = try session.saveDraftMessage( - payload: draftPayload, - for: cid, - cache: nil - ) - draft = try messageDTO.asModel() - }, completion: { error in - if let draft { - completion?(.success(DraftMessage(draft))) - } else if let error { - completion?(.failure(error)) - } - }) - case .failure(let error): - completion?(.failure(error)) + return newMessageDTO.asDraftRequestBody() + }) { writeResult in + switch writeResult { + case .failure(let error): + completion?(.failure(error)) + case .success(let requestBody): + self.apiClient.request( + endpoint: .updateDraftMessage(channelId: cid, requestBody: requestBody) + ) { [weak self] result in + switch result { + case .success(let response): + self?.database.write(converting: { session in + let draftPayload = response.draft + let messageDTO = try session.saveDraftMessage( + payload: draftPayload, + for: cid, + cache: nil + ) + let message = try messageDTO.asModel() + return DraftMessage(message) + }, completion: { + completion?($0) + }) + case .failure(let error): + completion?(.failure(error)) + } } } } @@ -115,28 +104,24 @@ class DraftMessagesRepository { func getDraft( for cid: ChannelId, threadId: MessageId?, - completion: ((Result) -> Void)? + completion: (@Sendable(Result) -> Void)? ) { apiClient.request( endpoint: .getDraftMessage(channelId: cid, threadId: threadId) ) { [weak self] result in switch result { case .success(let response): - var draft: ChatMessage? - self?.database.write({ session in + self?.database.write(converting: { session in let messageDTO = try session.saveDraftMessage( payload: response.draft, for: cid, cache: nil ) - draft = try messageDTO.asModel() - }) { error in - if let draft { - completion?(.success(DraftMessage(draft))) - } else if let error { - completion?(.failure(error)) - } - } + let message = try messageDTO.asModel() + return DraftMessage(message) + }, completion: { + completion?($0) + }) case .failure(let error): completion?(.failure(error)) } @@ -146,7 +131,7 @@ class DraftMessagesRepository { func deleteDraft( for cid: ChannelId, threadId: MessageId?, - completion: @escaping (Error?) -> Void + completion: @escaping @Sendable(Error?) -> Void ) { database.write { session in session.deleteDraftMessage(in: cid, threadId: threadId) diff --git a/Sources/StreamChat/Repositories/MessageRepository.swift b/Sources/StreamChat/Repositories/MessageRepository.swift index 392dad7b482..1ace2001ea6 100644 --- a/Sources/StreamChat/Repositories/MessageRepository.swift +++ b/Sources/StreamChat/Repositories/MessageRepository.swift @@ -12,7 +12,7 @@ enum MessageRepositoryError: LocalizedError { case failedToSendMessage(Error) } -class MessageRepository { +class MessageRepository: @unchecked Sendable { let database: DatabaseContainer let apiClient: APIClient @@ -23,7 +23,7 @@ class MessageRepository { func sendMessage( with messageId: MessageId, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { // Check the message with the given id is still in the DB. database.backgroundReadOnlyContext.perform { [weak self] in @@ -54,7 +54,7 @@ class MessageRepository { self?.database.write({ let messageDTO = $0.message(id: messageId) messageDTO?.localMessageState = .sending - }, completion: { error in + }, completion: { [weak self] error in if let error = error { log.error("Error changing localMessageState message with id \(messageId) to `sending`: \(error)") self?.markMessageAsFailedToSend(id: messageId) { @@ -69,7 +69,7 @@ class MessageRepository { skipPush: skipPush, skipEnrichUrl: skipEnrichUrl ) - self?.apiClient.request(endpoint: endpoint) { + self?.apiClient.request(endpoint: endpoint) { [weak self] in switch $0 { case let .success(payload): self?.saveSuccessfullySentMessage(cid: cid, message: payload.message) { result in @@ -90,9 +90,9 @@ class MessageRepository { } /// Marks the message's local status to failed and adds it to the offline retry which sends the message when connection comes back. - func scheduleOfflineRetry(for messageId: MessageId, completion: @escaping (Result) -> Void) { - var dataEndpoint: DataEndpoint! - var messageModel: ChatMessage! + func scheduleOfflineRetry(for messageId: MessageId, completion: @escaping @Sendable(Result) -> Void) { + nonisolated(unsafe) var dataEndpoint: DataEndpoint! + nonisolated(unsafe) var messageModel: ChatMessage! database.write { session in guard let dto = session.message(id: messageId) else { throw MessageRepositoryError.messageDoesNotExist @@ -133,9 +133,9 @@ class MessageRepository { func saveSuccessfullySentMessage( cid: ChannelId, message: MessagePayload, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { - var messageModel: ChatMessage! + nonisolated(unsafe) var messageModel: ChatMessage! database.write({ let messageDTO = try $0.saveMessage( payload: message, @@ -161,7 +161,7 @@ class MessageRepository { private func handleSendingMessageError( _ error: Error, messageId: MessageId, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { log.error("Sending the message with id \(messageId) failed with error: \(error)") @@ -188,7 +188,7 @@ class MessageRepository { } } - private func markMessageAsFailedToSend(id: MessageId, completion: @escaping () -> Void) { + private func markMessageAsFailedToSend(id: MessageId, completion: @escaping @Sendable() -> Void) { database.write({ let dto = $0.message(id: id) if dto?.localMessageState == .sending { @@ -205,11 +205,11 @@ class MessageRepository { }) } - func saveSuccessfullyEditedMessage(for id: MessageId, completion: @escaping () -> Void) { + func saveSuccessfullyEditedMessage(for id: MessageId, completion: @escaping @Sendable() -> Void) { updateMessage(withID: id, localState: nil, completion: { _ in completion() }) } - func saveSuccessfullyDeletedMessage(message: MessagePayload, completion: ((Error?) -> Void)? = nil) { + func saveSuccessfullyDeletedMessage(message: MessagePayload, completion: (@Sendable(Error?) -> Void)? = nil) { database.write({ session in guard let messageDTO = session.message(id: message.id), let cid = messageDTO.channel?.cid else { return } let deletedMessage = try session.saveMessage( @@ -238,34 +238,28 @@ class MessageRepository { /// - messageId: The message identifier. /// - store: A boolean indicating if the message should be stored to database or should only be retrieved /// - completion: The completion. Will be called with an error if something goes wrong, otherwise - will be called with `nil`. - func getMessage(cid: ChannelId, messageId: MessageId, store: Bool, completion: ((Result) -> Void)? = nil) { + func getMessage(cid: ChannelId, messageId: MessageId, store: Bool, completion: (@Sendable(Result) -> Void)? = nil) { let endpoint: Endpoint = .getMessage(messageId: messageId) apiClient.request(endpoint: endpoint) { switch $0 { case let .success(boxed): - var message: ChatMessage? - self.database.write({ session in - message = try session.saveMessage( + self.database.write(converting: { session in + let message = try session.saveMessage( payload: boxed.message, for: cid, syncOwnReactions: true, skipDraftUpdate: false, cache: nil - ).asModel() + ) + .asModel() if !store { // Force load attachments before discarding changes - _ = message?.attachmentCounts + _ = message.attachmentCounts self.database.writableContext.rollback() } - }, completion: { error in - if let error = error { - completion?(.failure(error)) - } else if let message = message { - completion?(.success(message)) - } else { - let error = ClientError.MessagePayloadSavingFailure("Missing message or error") - completion?(.failure(error)) - } + return message + }, completion: { result in + completion?(result) }) case let .failure(error): completion?(.failure(error)) @@ -299,25 +293,18 @@ class MessageRepository { } } - func updateMessage(withID id: MessageId, localState: LocalMessageState?, completion: @escaping (Result) -> Void) { - var message: ChatMessage? - database.write({ - let dto = $0.message(id: id) - dto?.localMessageState = localState - message = try dto?.asModel() - }, completion: { error in - if let error = error { - completion(.failure(error)) - } else { - completion(.success(message!)) - } - }) + func updateMessage(withID id: MessageId, localState: LocalMessageState?, completion: @escaping @Sendable(Result) -> Void) { + database.write(converting: { + guard let dto = $0.message(id: id) else { throw ClientError.MessageDoesNotExist(messageId: id) } + dto.localMessageState = localState + return try dto.asModel() + }, completion: completion) } func undoReactionAddition( on messageId: MessageId, type: MessageReactionType, - completion: (() -> Void)? = nil + completion: (@Sendable() -> Void)? = nil ) { database.write { let reaction = try $0.removeReaction(from: messageId, type: type, on: nil) @@ -334,7 +321,7 @@ class MessageRepository { on messageId: MessageId, type: MessageReactionType, score: Int, - completion: (() -> Void)? = nil + completion: (@Sendable() -> Void)? = nil ) { database.write { _ = try $0.addReaction(to: messageId, type: type, score: score, enforceUnique: false, extraData: [:], localState: .deletingFailed) diff --git a/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift b/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift index b1ee5236992..12d82c04f51 100644 --- a/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift +++ b/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift @@ -23,7 +23,7 @@ extension Endpoint { /// OfflineRequestsRepository handles both the enqueuing and the execution of offline requests when needed. /// When running the queued requests, it basically passes the requests on to the APIClient, and waits for its result. -class OfflineRequestsRepository { +class OfflineRequestsRepository: @unchecked Sendable { enum Constants { static let secondsInHour: Double = 3600 } @@ -51,7 +51,7 @@ class OfflineRequestsRepository { /// - If the requests succeeds -> The request is removed from the pending ones /// - If the request fails with a connection error -> The request is kept to be executed once the connection is back (we are not putting it back at the queue to make sure we respect the order) /// - If the request fails with any other error -> We are dismissing the request, and removing it from the queue - func runQueuedRequests(completion: @escaping () -> Void) { + func runQueuedRequests(completion: @escaping @Sendable() -> Void) { database.read { session in let dtos = session.allQueuedRequests() var requests = [Request]() @@ -113,8 +113,8 @@ class OfflineRequestsRepository { } completion: { [weak self] result in switch result { case .success(let pair): - self?.deleteRequests(with: pair.deleteIds, completion: { - self?.retryQueue.async { + self?.deleteRequests(with: pair.deleteIds, completion: { [weak self] in + self?.retryQueue.async { [weak self] in self?.executeRequests(pair.requests, completion: completion) } }) @@ -125,7 +125,7 @@ class OfflineRequestsRepository { } } - private func deleteRequests(with ids: Set, completion: @escaping () -> Void) { + private func deleteRequests(with ids: Set, completion: @escaping @Sendable() -> Void) { guard !ids.isEmpty else { completion() return @@ -139,7 +139,7 @@ class OfflineRequestsRepository { } } - private func executeRequests(_ requests: [Request], completion: @escaping () -> Void) { + private func executeRequests(_ requests: [Request], completion: @escaping @Sendable() -> Void) { let database = self.database let group = DispatchGroup() for request in requests { @@ -147,13 +147,10 @@ class OfflineRequestsRepository { let endpoint = request.endpoint group.enter() - let leave = { - group.leave() - } - let deleteQueuedRequestAndComplete = { + let deleteQueuedRequestAndComplete: @Sendable() -> Void = { database.write({ session in session.deleteQueuedRequest(id: id) - }, completion: { _ in leave() }) + }, completion: { _ in group.leave() }) } log.info("Executing queued offline request for /\(endpoint.path)", subsystems: .offlineSupport) @@ -172,7 +169,7 @@ class OfflineRequestsRepository { "Keeping offline request /\(endpoint.path) as there is no connection", subsystems: .offlineSupport ) - leave() + group.leave() case let .failure(error): log.info( "Request for /\(endpoint.path) failed: \(error)", @@ -192,7 +189,7 @@ class OfflineRequestsRepository { private func performDatabaseRecoveryActionsUponSuccess( for endpoint: DataEndpoint, data: Data, - completion: @escaping () -> Void + completion: @escaping @Sendable() -> Void ) { func decodeTo(_ type: T.Type) -> T? { try? JSONDecoder.stream.decode(T.self, from: data) @@ -222,7 +219,7 @@ class OfflineRequestsRepository { } } - func queueOfflineRequest(endpoint: DataEndpoint, completion: (() -> Void)? = nil) { + func queueOfflineRequest(endpoint: DataEndpoint, completion: (@Sendable() -> Void)? = nil) { guard endpoint.shouldBeQueuedOffline else { completion?() return diff --git a/Sources/StreamChat/Repositories/PollsRepository.swift b/Sources/StreamChat/Repositories/PollsRepository.swift index c39ec8fd1fe..ffdd0ba8f92 100644 --- a/Sources/StreamChat/Repositories/PollsRepository.swift +++ b/Sources/StreamChat/Repositories/PollsRepository.swift @@ -4,7 +4,7 @@ import Foundation -class PollsRepository { +class PollsRepository: @unchecked Sendable { let database: DatabaseContainer let apiClient: APIClient @@ -23,7 +23,7 @@ class PollsRepository { votingVisibility: String?, options: [PollOption]?, custom: [String: RawJSON]?, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { let request = CreatePollRequestBody( name: name, @@ -54,7 +54,7 @@ class PollsRepository { currentUserId: String?, query: PollVoteListQuery?, deleteExistingVotes: [PollVote], - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { guard let optionId, !optionId.isEmpty else { // No optimistic updates for answers. @@ -78,12 +78,11 @@ class PollsRepository { return } - var pollVote: PollVote? - database.write { session in + database.write(converting: { session in let voteId = PollVoteDTO.localVoteId(optionId: optionId, pollId: pollId, userId: currentUserId) let existing = try? session.pollVote(id: voteId, pollId: pollId) if existing == nil { - pollVote = try session.savePollVote( + let pollVote = try session.savePollVote( voteId: nil, pollId: pollId, optionId: optionId, @@ -92,14 +91,15 @@ class PollsRepository { query: query ) .asModel() + for toDelete in deleteExistingVotes { + _ = try? session.removePollVote(with: toDelete.id, pollId: toDelete.pollId) + } + return pollVote } else { throw ClientError.PollVoteAlreadyExists() } - for toDelete in deleteExistingVotes { - _ = try? session.removePollVote(with: toDelete.id, pollId: toDelete.pollId) - } - } completion: { [weak self] error in - if let error { + }, completion: { [weak self] result in + if let error = result.error { completion?(error) return } @@ -117,8 +117,8 @@ class PollsRepository { pollId: pollId, vote: request ) - ) { - if $0.isError, $0.error?.isBackendErrorWith400StatusCode == false, let pollVote { + ) { [weak self] in + if $0.isError, $0.error?.isBackendErrorWith400StatusCode == false, let pollVote = result.value { self?.database.write { session in _ = try? session.removePollVote(with: pollVote.id, pollId: pollVote.pollId) for vote in deleteExistingVotes { @@ -135,60 +135,64 @@ class PollsRepository { } completion?($0.error) } - } + }) } func removePollVote( messageId: MessageId, pollId: String, voteId: String, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { - var exists = false - var answerText: String? - var optionId: String? - var userId: UserId? - var filterHash: String? - database.write { session in + struct WriteResponse { + var exists = false + var answerText: String? + var optionId: String? + var userId: UserId? + var filterHash: String? + } + database.write(converting: { session in let voteDto = try session.removePollVote(with: voteId, pollId: pollId) - exists = voteDto != nil - filterHash = voteDto?.queries?.first?.filterHash - answerText = voteDto?.answerText - optionId = voteDto?.optionId - userId = voteDto?.user?.id - } completion: { [weak self] error in - if error == nil { + var writeResponse = WriteResponse() + writeResponse.exists = voteDto != nil + writeResponse.filterHash = voteDto?.queries?.first?.filterHash + writeResponse.answerText = voteDto?.answerText + writeResponse.optionId = voteDto?.optionId + writeResponse.userId = voteDto?.user?.id + return writeResponse + }, completion: { [weak self] result in + if let writeResponse = result.value { self?.apiClient.request( endpoint: .removePollVote( messageId: messageId, pollId: pollId, voteId: voteId ) - ) { - if $0.error != nil, $0.error?.isBackendNotFound404StatusCode == false, exists { + ) { [weak self] in + if $0.error != nil, $0.error?.isBackendNotFound404StatusCode == false, writeResponse.exists { self?.database.write { session in _ = try session.savePollVote( voteId: voteId, pollId: pollId, - optionId: optionId, - answerText: answerText, - userId: userId, + optionId: writeResponse.optionId, + answerText: writeResponse.answerText, + userId: writeResponse.userId, query: nil ) - try? session.linkVote(with: voteId, in: pollId, to: filterHash) + try? session.linkVote(with: voteId, in: pollId, to: writeResponse.filterHash) } } completion?($0.error) } } else { - completion?(error) + completion?(result.error) } - } + }) } func closePoll( pollId: String, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { let request = UpdatePollPartialRequestBody( pollId: pollId, @@ -203,7 +207,7 @@ class PollsRepository { func deletePoll( pollId: String, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .deletePoll(pollId: pollId)) { [weak self] in if $0.error == nil { @@ -220,7 +224,7 @@ class PollsRepository { text: String, position: Int? = nil, custom: [String: RawJSON]? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { let request = CreatePollOptionRequestBody( pollId: pollId, @@ -238,21 +242,21 @@ class PollsRepository { func queryPollVotes( query: PollVoteListQuery, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { apiClient.request( endpoint: .queryPollVotes(pollId: query.pollId, query: query) ) { [weak self] (result: Result) in switch result { case let .success(payload): - var votes: [PollVote] = [] - self?.database.write({ session in - votes = try session.savePollVotes(payload: payload, query: query, cache: nil).map { try $0.asModel() } - }, completion: { error in - if let error = error { - completion?(.failure(error)) - } else { + self?.database.write(converting: { session in + try session.savePollVotes(payload: payload, query: query, cache: nil).map { try $0.asModel() } + }, completion: { result in + switch result { + case .success(let votes): completion?(.success(.init(votes: votes, next: payload.next))) + case .failure(let error): + completion?(.failure(error)) } }) case let .failure(error): @@ -268,7 +272,7 @@ class PollsRepository { prev: String?, sort: [SortParamRequest?], filter: [String: RawJSON]?, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { let request = QueryPollVotesRequestBody( pollId: pollId, @@ -317,31 +321,31 @@ class PollsRepository { } extension ClientError { - final class PollDoesNotExist: ClientError { + final class PollDoesNotExist: ClientError, @unchecked Sendable { init(pollId: String) { super.init("There is no `PollDTO` instance in the DB matching id: \(pollId).") } } - final class PollOptionDoesNotExist: ClientError { + final class PollOptionDoesNotExist: ClientError, @unchecked Sendable { init(optionId: String) { super.init("There is no `PollOptionDTO` instance in the DB matching id: \(optionId).") } } - final class PollVoteDoesNotExist: ClientError { + final class PollVoteDoesNotExist: ClientError, @unchecked Sendable { init(voteId: String) { super.init("There is no `PollVoteDTO` instance in the DB matching id: \(voteId).") } } - public final class PollVoteAlreadyExists: ClientError { + public final class PollVoteAlreadyExists: ClientError, @unchecked Sendable { public init() { super.init("There is already `PollVoteDTO` instance in the DB.") } } - final class InvalidInput: ClientError { + final class InvalidInput: ClientError, @unchecked Sendable { init() { super.init("Invalid input provided to the method") } diff --git a/Sources/StreamChat/Repositories/SyncOperations.swift b/Sources/StreamChat/Repositories/SyncOperations.swift index bead0bbac74..987b5ffb12c 100644 --- a/Sources/StreamChat/Repositories/SyncOperations.swift +++ b/Sources/StreamChat/Repositories/SyncOperations.swift @@ -5,12 +5,12 @@ import Foundation /// A final class that holds the context for the ongoing operations during the sync process -final class SyncContext { +final class SyncContext: @unchecked Sendable { let lastSyncAt: Date - var localChannelIds: Set = Set() - var synchedChannelIds: Set = Set() - var watchedAndSynchedChannelIds: Set = Set() - var unwantedChannelIds: Set = Set() + @Atomic var localChannelIds: Set = Set() + @Atomic var synchedChannelIds: Set = Set() + @Atomic var watchedAndSynchedChannelIds: Set = Set() + @Atomic var unwantedChannelIds: Set = Set() init(lastSyncAt: Date) { self.lastSyncAt = lastSyncAt @@ -30,7 +30,7 @@ final class ActiveChannelIdsOperation: AsyncOperation, @unchecked Sendable { return } - let completion: () -> Void = { + let completion: @Sendable() -> Void = { context.localChannelIds = Set(context.localChannelIds) log.info("Found \(context.localChannelIds.count) active channels", subsystems: .offlineSupport) done(.continue) @@ -50,10 +50,12 @@ final class ActiveChannelIdsOperation: AsyncOperation, @unchecked Sendable { completion() } else { // Main actor requirement + let chats = syncRepository.activeChats.allObjects + let channelLists = syncRepository.activeChannelLists.allObjects DispatchQueue.main.async { - context.localChannelIds.formUnion(syncRepository.activeChats.allObjects.compactMap { try? $0.cid }) + context.localChannelIds.formUnion(chats.compactMap { try? $0.cid }) context.localChannelIds.formUnion( - syncRepository.activeChannelLists.allObjects + channelLists .map(\.state.channels) .flatMap { $0 } .map(\.cid) diff --git a/Sources/StreamChat/Repositories/SyncRepository.swift b/Sources/StreamChat/Repositories/SyncRepository.swift index c3f16dd9c90..417f1ecf197 100644 --- a/Sources/StreamChat/Repositories/SyncRepository.swift +++ b/Sources/StreamChat/Repositories/SyncRepository.swift @@ -4,7 +4,7 @@ import Foundation -enum SyncError: Error { +enum SyncError: Error, Sendable { case noNeedToSync case tooManyEvents(Error) case syncEndpointFailed(Error) @@ -23,7 +23,7 @@ enum SyncError: Error { /// This class is in charge of the synchronization of our local storage with the remote. /// When executing a sync, it will remove outdated elements, and will refresh the content to always show the latest data. -class SyncRepository { +class SyncRepository: @unchecked Sendable { private enum Constants { static let maximumDaysSinceLastSync = 30 } @@ -42,7 +42,7 @@ class SyncRepository { let activeChats = ThreadSafeWeakCollection() let activeChannelLists = ThreadSafeWeakCollection() - private lazy var operationQueue: OperationQueue = { + private let operationQueue: OperationQueue = { let operationQueue = OperationQueue() operationQueue.maxConcurrentOperationCount = 1 operationQueue.name = "com.stream.sync-repository" @@ -117,7 +117,7 @@ class SyncRepository { // MARK: - Syncing - func syncLocalState(completion: @escaping () -> Void) { + func syncLocalState(completion: @escaping @Sendable() -> Void) { cancelRecoveryFlow() getUser { [weak self] in @@ -129,9 +129,9 @@ class SyncRepository { guard let lastSyncAt = currentUser.lastSynchedEventDate?.bridgeDate else { log.info("It's the first session of the current user, skipping recovery flow", subsystems: .offlineSupport) - self?.updateUserValue({ $0?.lastSynchedEventDate = DBDate() }) { _ in + self?.updateLastSyncAt(with: Date(), completion: { _ in completion() - } + }) return } self?.syncLocalState(lastSyncAt: lastSyncAt, completion: completion) @@ -154,7 +154,7 @@ class SyncRepository { /// * channel controllers targeting other channels /// * no channel lists active, but channel controllers are /// 4. Re-watch channels what we were watching before disconnect - private func syncLocalState(lastSyncAt: Date, completion: @escaping () -> Void) { + private func syncLocalState(lastSyncAt: Date, completion: @escaping @Sendable() -> Void) { let context = SyncContext(lastSyncAt: lastSyncAt) var operations: [Operation] = [] let start = CFAbsoluteTimeGetCurrent() @@ -216,7 +216,7 @@ class SyncRepository { channelIds: [ChannelId], lastSyncAt: Date, isRecovery: Bool, - completion: @escaping (Result<[ChannelId], SyncError>) -> Void + completion: @escaping @Sendable(Result<[ChannelId], SyncError>) -> Void ) { guard lastSyncAt.numberOfDaysUntilNow < Constants.maximumDaysSinceLastSync else { updateLastSyncAt(with: Date()) { error in @@ -249,7 +249,7 @@ class SyncRepository { using date: Date, channelIds: [ChannelId], isRecoveryRequest: Bool, - completion: @escaping (Result<[ChannelId], SyncError>) -> Void + completion: @escaping @Sendable(Result<[ChannelId], SyncError>) -> Void ) { log.info("Synching events for existing channels since \(date)", subsystems: .offlineSupport) @@ -259,11 +259,11 @@ class SyncRepository { } let endpoint: Endpoint = .missingEvents(since: date, cids: channelIds) - let requestCompletion: (Result) -> Void = { [weak self] result in + let requestCompletion: @Sendable(Result) -> Void = { [weak self] result in switch result { case let .success(payload): log.info("Processing pending events. Count \(payload.eventPayloads.count)", subsystems: .offlineSupport) - self?.processMissingEventsPayload(payload) { + self?.processMissingEventsPayload(payload) { [weak self] in self?.updateLastSyncAt(with: payload.eventPayloads.last?.createdAt ?? date, completion: { error in if let error = error { completion(.failure(error)) @@ -299,13 +299,21 @@ class SyncRepository { } } - private func updateLastSyncAt(with date: Date, completion: @escaping (SyncError?) -> Void) { - updateUserValue({ - $0?.lastSynchedEventDate = date.bridgeDate - }, completion: completion) + private func updateLastSyncAt(with date: Date, completion: @escaping @Sendable(SyncError?) -> Void) { + database.write { session in + session.currentUser?.lastSynchedEventDate = date.bridgeDate + } completion: { error in + if let error = error { + log.error("Failed updating value: \(error)", subsystems: .offlineSupport) + completion(.couldNotUpdateUserValue(error)) + } else { + log.info("Updated user value", subsystems: .offlineSupport) + completion(nil) + } + } } - private func processMissingEventsPayload(_ payload: MissingEventsPayload, completion: @escaping () -> Void) { + private func processMissingEventsPayload(_ payload: MissingEventsPayload, completion: @escaping @Sendable() -> Void) { eventNotificationCenter.process(payload.eventPayloads.asEvents(), postNotifications: false) { log.info( "Successfully processed pending events. Count \(payload.eventPayloads.count)", @@ -315,24 +323,6 @@ class SyncRepository { } } - private func updateUserValue( - _ block: @escaping (inout CurrentUserDTO?) -> Void, - completion: ((SyncError?) -> Void)? = nil - ) { - database.write { session in - var currentUser = session.currentUser - block(¤tUser) - } completion: { error in - if let error = error { - log.error("Failed updating value: \(error)", subsystems: .offlineSupport) - completion?(.couldNotUpdateUserValue(error)) - } else { - log.info("Updated user value", subsystems: .offlineSupport) - completion?(nil) - } - } - } - func queueOfflineRequest(endpoint: DataEndpoint) { guard config.isLocalStorageEnabled else { return } offlineRequestsRepository.queueOfflineRequest(endpoint: endpoint) diff --git a/Sources/StreamChat/Repositories/ThreadsRepository.swift b/Sources/StreamChat/Repositories/ThreadsRepository.swift index 2d87a6b2069..4f9104539ef 100644 --- a/Sources/StreamChat/Repositories/ThreadsRepository.swift +++ b/Sources/StreamChat/Repositories/ThreadsRepository.swift @@ -4,12 +4,12 @@ import CoreData -struct ThreadListResponse { +struct ThreadListResponse: Sendable { var threads: [ChatThread] var next: String? } -class ThreadsRepository { +class ThreadsRepository: @unchecked Sendable { let database: DatabaseContainer let apiClient: APIClient @@ -20,34 +20,26 @@ class ThreadsRepository { func loadThreads( query: ThreadListQuery, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { apiClient.request(endpoint: .threads(query: query)) { [weak self] result in switch result { case .success(let threadListPayload): - var threads: [ChatThread] = [] - self?.database.write({ session in + self?.database.write(converting: { session in if query.next == nil { /// For now, there is no `ThreadListQuery.filter` support. /// So we only have 1 thread list, which is all threads. /// So when fetching the first page, we need to cleanup all threads. try session.deleteAllThreads() } - threads = try session.saveThreadList(payload: threadListPayload).map { + let threads = try session.saveThreadList(payload: threadListPayload).map { try $0.asModel() } - }, completion: { error in - if let error = error { - completion(.failure(error)) - } else { - completion(.success( - ThreadListResponse( - threads: threads, - next: threadListPayload.next - ) - )) - } - }) + return ThreadListResponse( + threads: threads, + next: threadListPayload.next + ) + }, completion: completion) case .failure(let error): completion(.failure(error)) } diff --git a/Sources/StreamChat/StateLayer/ChannelList.swift b/Sources/StreamChat/StateLayer/ChannelList.swift index ad33c9bfcd9..5f43800e221 100644 --- a/Sources/StreamChat/StateLayer/ChannelList.swift +++ b/Sources/StreamChat/StateLayer/ChannelList.swift @@ -5,15 +5,15 @@ import Foundation /// An object which represents a list of `ChatChannel`s for the specified channel query. -public class ChannelList { +public class ChannelList: @unchecked Sendable { private let channelListUpdater: ChannelListUpdater private let client: ChatClient - private let stateBuilder: StateBuilder + @MainActor private var stateBuilder: StateBuilder let query: ChannelListQuery init( query: ChannelListQuery, - dynamicFilter: ((ChatChannel) -> Bool)?, + dynamicFilter: (@Sendable(ChatChannel) -> Bool)?, client: ChatClient, environment: Environment = .init() ) { @@ -39,7 +39,7 @@ public class ChannelList { // MARK: - Accessing the State /// An observable object representing the current state of the channel list. - @MainActor public lazy var state: ChannelListState = stateBuilder.build() + @MainActor public var state: ChannelListState { stateBuilder.reuseOrBuild() } /// Fetches the most recent state from the server and updates the local store. /// @@ -91,15 +91,15 @@ public class ChannelList { } extension ChannelList { - struct Environment { - var channelListUpdater: ( + struct Environment: Sendable { + var channelListUpdater: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient - ) -> ChannelListUpdater = ChannelListUpdater.init + ) -> ChannelListUpdater = { ChannelListUpdater(database: $0, apiClient: $1) } - var stateBuilder: @MainActor( + var stateBuilder: @Sendable @MainActor( _ query: ChannelListQuery, - _ dynamicFilter: ((ChatChannel) -> Bool)?, + _ dynamicFilter: (@Sendable(ChatChannel) -> Bool)?, _ clientConfig: ChatClientConfig, _ channelListUpdater: ChannelListUpdater, _ database: DatabaseContainer, diff --git a/Sources/StreamChat/StateLayer/ChannelListState+Observer.swift b/Sources/StreamChat/StateLayer/ChannelListState+Observer.swift index abe5fa36497..0c26c184f9d 100644 --- a/Sources/StreamChat/StateLayer/ChannelListState+Observer.swift +++ b/Sources/StreamChat/StateLayer/ChannelListState+Observer.swift @@ -17,7 +17,7 @@ extension ChannelListState { init( query: ChannelListQuery, - dynamicFilter: ((ChatChannel) -> Bool)?, + dynamicFilter: (@Sendable(ChatChannel) -> Bool)?, clientConfig: ChatClientConfig, channelListUpdater: ChannelListUpdater, database: DatabaseContainer, @@ -50,7 +50,7 @@ extension ChannelListState { } struct Handlers { - let channelsDidChange: @MainActor(StreamCollection) async -> Void + let channelsDidChange: @Sendable @MainActor(StreamCollection) async -> Void } func start(with handlers: Handlers) -> StreamCollection { diff --git a/Sources/StreamChat/StateLayer/ChannelListState.swift b/Sources/StreamChat/StateLayer/ChannelListState.swift index d33fb0bef36..7741de3ee1a 100644 --- a/Sources/StreamChat/StateLayer/ChannelListState.swift +++ b/Sources/StreamChat/StateLayer/ChannelListState.swift @@ -10,7 +10,7 @@ import Foundation init( query: ChannelListQuery, - dynamicFilter: ((ChatChannel) -> Bool)?, + dynamicFilter: (@Sendable(ChatChannel) -> Bool)?, clientConfig: ChatClientConfig, channelListUpdater: ChannelListUpdater, database: DatabaseContainer, diff --git a/Sources/StreamChat/StateLayer/Chat.swift b/Sources/StreamChat/StateLayer/Chat.swift index 8beb2287dbc..218ec580a72 100644 --- a/Sources/StreamChat/StateLayer/Chat.swift +++ b/Sources/StreamChat/StateLayer/Chat.swift @@ -6,7 +6,7 @@ import Combine import Foundation /// An object which represents a `ChatChannel`. -public class Chat { +public class Chat: @unchecked Sendable { private let channelUpdater: ChannelUpdater private let client: ChatClient private let databaseContainer: DatabaseContainer @@ -14,7 +14,7 @@ public class Chat { private let eventSender: EventSender private let memberUpdater: ChannelMemberUpdater private let messageUpdater: MessageUpdater - private let stateBuilder: StateBuilder + @MainActor private var stateBuilder: StateBuilder private let typingEventsSender: TypingEventsSender init( @@ -67,7 +67,7 @@ public class Chat { // MARK: - Accessing the State /// An observable object representing the current state of the channel. - @MainActor public lazy var state: ChatState = stateBuilder.build() + @MainActor public var state: ChatState { stateBuilder.reuseOrBuild() } /// Fetches the most recent state from the server and updates the local store. /// @@ -1217,7 +1217,7 @@ public class Chat { /// - Returns: A cancellable instance, which you use when you end the subscription. Deallocation of the result will tear down the subscription stream. public func subscribe( toEvent event: E.Type, - handler: @escaping (E) -> Void + handler: @escaping @Sendable(E) -> Void ) -> AnyCancellable where E: Event { eventNotificationCenter.subscribe( to: event, @@ -1234,7 +1234,7 @@ public class Chat { /// - Parameter handler: The handler closure which is called when the event happens. /// /// - Returns: A cancellable instance, which you use when you end the subscription. Deallocation of the result will tear down the subscription stream. - public func subscribe(_ handler: @escaping (Event) -> Void) -> AnyCancellable { + public func subscribe(_ handler: @escaping @Sendable(Event) -> Void) -> AnyCancellable { eventNotificationCenter.subscribe( handler: { [weak self] event in self?.dispatchSubscribeHandler(event, callback: handler) @@ -1441,7 +1441,7 @@ public class Chat { public func uploadAttachment( with localFileURL: URL, type: AttachmentType, - progress: ((Double) -> Void)? = nil + progress: (@Sendable(Double) -> Void)? = nil ) async throws -> UploadedAttachment { try await channelUpdater.uploadFile( type: type, @@ -1486,7 +1486,7 @@ extension Chat { } } - func dispatchSubscribeHandler(_ event: E, callback: @escaping (E) -> Void) where E: Event { + func dispatchSubscribeHandler(_ event: E, callback: @escaping @Sendable(E) -> Void) where E: Event { Task.mainActor { guard let cid = try? self.cid else { return } guard EventNotificationCenter.channelFilter(cid: cid, event: event) else { return } @@ -1526,8 +1526,8 @@ extension Chat { // MARK: - Environment extension Chat { - struct Environment { - var chatStateBuilder: @MainActor( + struct Environment: Sendable { + var chatStateBuilder: @Sendable @MainActor( _ channelQuery: ChannelQuery, _ messageOrder: MessageOrdering, _ memberSorting: [Sorting], @@ -1545,40 +1545,61 @@ extension Chat { ) } - var channelUpdaterBuilder: ( + var channelUpdaterBuilder: @Sendable( _ channelRepository: ChannelRepository, _ messageRepository: MessageRepository, _ paginationStateHandler: MessagesPaginationStateHandling, _ database: DatabaseContainer, _ apiClient: APIClient - ) -> ChannelUpdater = ChannelUpdater.init + ) -> ChannelUpdater = { + ChannelUpdater( + channelRepository: $0, + messageRepository: $1, + paginationStateHandler: $2, + database: $3, + apiClient: $4 + ) + } - var eventSenderBuilder: ( + var eventSenderBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient - ) -> EventSender = EventSender.init + ) -> EventSender = { EventSender(database: $0, apiClient: $1) } - var memberUpdaterBuilder: ( + var memberUpdaterBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient - ) -> ChannelMemberUpdater = ChannelMemberUpdater.init + ) -> ChannelMemberUpdater = { ChannelMemberUpdater(database: $0, apiClient: $1) } - var messageUpdaterBuilder: ( + var messageUpdaterBuilder: @Sendable( _ isLocalStorageEnabled: Bool, _ messageRepository: MessageRepository, _ database: DatabaseContainer, _ apiClient: APIClient - ) -> MessageUpdater = MessageUpdater.init + ) -> MessageUpdater = { + MessageUpdater( + isLocalStorageEnabled: $0, + messageRepository: $1, + database: $2, + apiClient: $3 + ) + } - var readStateHandlerBuilder: ( + var readStateHandlerBuilder: @Sendable( _ authenticationRepository: AuthenticationRepository, _ channelUpdater: ChannelUpdater, _ messageRepository: MessageRepository - ) -> ReadStateHandler = ReadStateHandler.init + ) -> ReadStateHandler = { + ReadStateHandler( + authenticationRepository: $0, + channelUpdater: $1, + messageRepository: $2 + ) + } - var typingEventsSenderBuilder: ( + var typingEventsSenderBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient - ) -> TypingEventsSender = TypingEventsSender.init + ) -> TypingEventsSender = { TypingEventsSender(database: $0, apiClient: $1) } } } diff --git a/Sources/StreamChat/StateLayer/ChatClient+Factory.swift b/Sources/StreamChat/StateLayer/ChatClient+Factory.swift index c01ffcf364e..8e36511562b 100644 --- a/Sources/StreamChat/StateLayer/ChatClient+Factory.swift +++ b/Sources/StreamChat/StateLayer/ChatClient+Factory.swift @@ -47,7 +47,7 @@ extension ChatClient { /// - Returns: An instance of ``ChannelList`` which represents actions and the state of the list. public func makeChannelList( with query: ChannelListQuery, - dynamicFilter: ((ChatChannel) -> Bool)? = nil + dynamicFilter: (@Sendable(ChatChannel) -> Bool)? = nil ) -> ChannelList { ChannelList(query: query, dynamicFilter: dynamicFilter, client: self) } diff --git a/Sources/StreamChat/StateLayer/ChatState+Observer.swift b/Sources/StreamChat/StateLayer/ChatState+Observer.swift index 4b37ca2cc8a..f36defc97d4 100644 --- a/Sources/StreamChat/StateLayer/ChatState+Observer.swift +++ b/Sources/StreamChat/StateLayer/ChatState+Observer.swift @@ -56,10 +56,10 @@ extension ChatState { } struct Handlers { - let channelDidChange: @MainActor(ChatChannel?) async -> Void - let membersDidChange: @MainActor(StreamCollection) async -> Void - let messagesDidChange: @MainActor(StreamCollection) async -> Void - let watchersDidChange: @MainActor(StreamCollection) async -> Void + let channelDidChange: @Sendable @MainActor(ChatChannel?) async -> Void + let membersDidChange: @Sendable @MainActor(StreamCollection) async -> Void + let messagesDidChange: @Sendable @MainActor(StreamCollection) async -> Void + let watchersDidChange: @Sendable @MainActor(StreamCollection) async -> Void } @MainActor func start( diff --git a/Sources/StreamChat/StateLayer/ConnectedUser.swift b/Sources/StreamChat/StateLayer/ConnectedUser.swift index 8989c7c2611..fbc762b69a1 100644 --- a/Sources/StreamChat/StateLayer/ConnectedUser.swift +++ b/Sources/StreamChat/StateLayer/ConnectedUser.swift @@ -5,10 +5,10 @@ import Foundation /// An object which represents the currently logged in user. -public final class ConnectedUser { +public final class ConnectedUser: Sendable { private let authenticationRepository: AuthenticationRepository private let currentUserUpdater: CurrentUserUpdater - private let stateBuilder: StateBuilder + @MainActor private var stateBuilder: StateBuilder private let userUpdater: UserUpdater init(user: CurrentChatUser, client: ChatClient, environment: Environment = .init()) { @@ -32,7 +32,7 @@ public final class ConnectedUser { // MARK: - Accessing the State /// An observable object representing the current state of the user. - @MainActor public lazy var state: ConnectedUserState = stateBuilder.build() + @MainActor public var state: ConnectedUserState { stateBuilder.reuseOrBuild() } // MARK: - Connected User Data @@ -209,19 +209,26 @@ public final class ConnectedUser { } extension ConnectedUser { - struct Environment { - var stateBuilder: @MainActor( + struct Environment: Sendable { + var stateBuilder: @Sendable @MainActor( _ user: CurrentChatUser, _ database: DatabaseContainer ) -> ConnectedUserState = { @MainActor in ConnectedUserState(user: $0, database: $1) } - var currentUserUpdaterBuilder = CurrentUserUpdater.init + var currentUserUpdaterBuilder: @Sendable( + _ database: DatabaseContainer, + _ apiClient: APIClient + ) -> CurrentUserUpdater = { + CurrentUserUpdater(database: $0, apiClient: $1) + } - var userUpdaterBuilder: ( + var userUpdaterBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient - ) -> UserUpdater = UserUpdater.init + ) -> UserUpdater = { + UserUpdater(database: $0, apiClient: $1) + } } } diff --git a/Sources/StreamChat/StateLayer/ConnectedUserState+Observer.swift b/Sources/StreamChat/StateLayer/ConnectedUserState+Observer.swift index efa48851980..ea917519c1d 100644 --- a/Sources/StreamChat/StateLayer/ConnectedUserState+Observer.swift +++ b/Sources/StreamChat/StateLayer/ConnectedUserState+Observer.swift @@ -17,7 +17,7 @@ extension ConnectedUserState { } struct Handlers { - let userDidChange: @MainActor(CurrentChatUser) async -> Void + let userDidChange: @Sendable @MainActor(CurrentChatUser) async -> Void } func start(with handlers: Handlers) -> CurrentChatUser? { diff --git a/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift b/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift index 2552add4c56..3d9725b8d2c 100644 --- a/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift +++ b/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift @@ -83,7 +83,7 @@ extension StateLayerDatabaseObserver where ResultType == EntityResult { /// - Parameter didChange: The callback which is triggered when the observed item changes. Runs on the ``MainActor``. /// /// - Returns: Returns the current state of the item in the local database. - func startObserving(didChange: @escaping @MainActor(Item?) async -> Void) throws -> Item? { + func startObserving(didChange: @escaping @Sendable @MainActor(Item?) async -> Void) throws -> Item? where Item: Sendable { try startObserving(onContextDidChange: { item, _ in Task.mainActor { await didChange(item) } }) } @@ -168,7 +168,7 @@ extension StateLayerDatabaseObserver where ResultType == ListResult { /// - Parameter didChange: The callback which is triggered when the observed item changes. Runs on the ``MainActor``. /// /// - Returns: Returns the current state of items in the local database. - func startObserving(didChange: @escaping @MainActor(StreamCollection) async -> Void) throws -> StreamCollection { + func startObserving(didChange: @escaping @Sendable @MainActor(StreamCollection) async -> Void) throws -> StreamCollection where Item: Sendable { try startObserving(onContextDidChange: { items, _ in Task.mainActor { await didChange(items) } }) diff --git a/Sources/StreamChat/StateLayer/MemberList.swift b/Sources/StreamChat/StateLayer/MemberList.swift index 868148dbab0..9aa360c0abe 100644 --- a/Sources/StreamChat/StateLayer/MemberList.swift +++ b/Sources/StreamChat/StateLayer/MemberList.swift @@ -5,10 +5,10 @@ import Foundation /// An object which represents a list of `ChatChannelMember` for the specified channel. -public final class MemberList { +public final class MemberList: Sendable { private let query: ChannelMemberListQuery private let memberListUpdater: ChannelMemberListUpdater - private let stateBuilder: StateBuilder + @MainActor private var stateBuilder: StateBuilder init(query: ChannelMemberListQuery, client: ChatClient, environment: Environment = .init()) { self.query = query @@ -27,7 +27,7 @@ public final class MemberList { // MARK: - Accessing the State /// An observable object representing the current state of the member list. - @MainActor public lazy var state: MemberListState = stateBuilder.build() + @MainActor public var state: MemberListState { stateBuilder.reuseOrBuild() } /// Fetches the most recent state from the server and updates the local store. /// @@ -64,13 +64,15 @@ public final class MemberList { } extension MemberList { - struct Environment { - var memberListUpdaterBuilder: ( + struct Environment: Sendable { + var memberListUpdaterBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient - ) -> ChannelMemberListUpdater = ChannelMemberListUpdater.init + ) -> ChannelMemberListUpdater = { + ChannelMemberListUpdater(database: $0, apiClient: $1) + } - var stateBuilder: @MainActor( + var stateBuilder: @Sendable @MainActor( _ query: ChannelMemberListQuery, _ database: DatabaseContainer ) -> MemberListState = { @MainActor in @@ -78,11 +80,3 @@ extension MemberList { } } } - -private extension ChannelMemberListQuery { - func withPagination(_ pagination: Pagination) -> Self { - var result = self - result.pagination = pagination - return result - } -} diff --git a/Sources/StreamChat/StateLayer/MemberListState+Observer.swift b/Sources/StreamChat/StateLayer/MemberListState+Observer.swift index 99961dbe543..d4fb6f149ba 100644 --- a/Sources/StreamChat/StateLayer/MemberListState+Observer.swift +++ b/Sources/StreamChat/StateLayer/MemberListState+Observer.swift @@ -18,7 +18,7 @@ extension MemberListState { } struct Handlers { - let membersDidChange: @MainActor(StreamCollection) async -> Void + let membersDidChange: @Sendable @MainActor(StreamCollection) async -> Void } func start(with handlers: Handlers) -> StreamCollection { diff --git a/Sources/StreamChat/StateLayer/MessageSearch.swift b/Sources/StreamChat/StateLayer/MessageSearch.swift index 89bdb9b1863..5b34f81ca1d 100644 --- a/Sources/StreamChat/StateLayer/MessageSearch.swift +++ b/Sources/StreamChat/StateLayer/MessageSearch.swift @@ -5,10 +5,10 @@ import Foundation /// An object which represents a list of ``ChatMessage`` for the specified search query. -public class MessageSearch { +public class MessageSearch: @unchecked Sendable { private let authenticationRepository: AuthenticationRepository private let messageUpdater: MessageUpdater - private let stateBuilder: StateBuilder + @MainActor private var stateBuilder: StateBuilder let explicitFilterHash = UUID().uuidString init(client: ChatClient, environment: Environment = .init()) { @@ -25,7 +25,7 @@ public class MessageSearch { // MARK: - Accessing the State /// An observable object representing the current state of the search. - @MainActor public lazy var state: MessageSearchState = stateBuilder.build() + @MainActor public var state: MessageSearchState { stateBuilder.reuseOrBuild() } // MARK: - Search Results and Pagination @@ -100,15 +100,22 @@ public class MessageSearch { } extension MessageSearch { - struct Environment { - var messageUpdaterBuilder: ( + struct Environment: Sendable { + var messageUpdaterBuilder: @Sendable( _ isLocalStorageEnabled: Bool, _ messageRepository: MessageRepository, _ database: DatabaseContainer, _ apiClient: APIClient - ) -> MessageUpdater = MessageUpdater.init + ) -> MessageUpdater = { + MessageUpdater( + isLocalStorageEnabled: $0, + messageRepository: $1, + database: $2, + apiClient: $3 + ) + } - var stateBuilder: @MainActor( + var stateBuilder: @Sendable @MainActor( _ database: DatabaseContainer ) -> MessageSearchState = { @MainActor in MessageSearchState(database: $0) diff --git a/Sources/StreamChat/StateLayer/MessageSearchState+Observer.swift b/Sources/StreamChat/StateLayer/MessageSearchState+Observer.swift index 479f8a775a3..8df33847b1c 100644 --- a/Sources/StreamChat/StateLayer/MessageSearchState+Observer.swift +++ b/Sources/StreamChat/StateLayer/MessageSearchState+Observer.swift @@ -19,7 +19,7 @@ extension MessageSearchState { } struct Handlers { - let messagesDidChange: @MainActor(StreamCollection) async -> Void + let messagesDidChange: @Sendable @MainActor(StreamCollection) async -> Void } private var handlers: Handlers? diff --git a/Sources/StreamChat/StateLayer/MessageState+Observer.swift b/Sources/StreamChat/StateLayer/MessageState+Observer.swift index 22e015f0fec..4c6ee7c62e3 100644 --- a/Sources/StreamChat/StateLayer/MessageState+Observer.swift +++ b/Sources/StreamChat/StateLayer/MessageState+Observer.swift @@ -47,9 +47,9 @@ extension MessageState { } struct Handlers { - let messageDidChange: @MainActor(ChatMessage) async -> Void - let reactionsDidChange: @MainActor(StreamCollection) async -> Void - let repliesDidChange: @MainActor(StreamCollection) async -> Void + let messageDidChange: @Sendable @MainActor(ChatMessage) async -> Void + let reactionsDidChange: @Sendable @MainActor(StreamCollection) async -> Void + let repliesDidChange: @Sendable @MainActor(StreamCollection) async -> Void } func start( diff --git a/Sources/StreamChat/StateLayer/ReactionList.swift b/Sources/StreamChat/StateLayer/ReactionList.swift index 28ac820941a..4869cd61855 100644 --- a/Sources/StreamChat/StateLayer/ReactionList.swift +++ b/Sources/StreamChat/StateLayer/ReactionList.swift @@ -5,10 +5,10 @@ import Foundation /// An object which represents a list of `ChatMessageReaction` for the specified query. -public final class ReactionList { +public final class ReactionList: Sendable { private let query: ReactionListQuery private let reactionListUpdater: ReactionListUpdater - private let stateBuilder: StateBuilder + @MainActor private var stateBuilder: StateBuilder init(query: ReactionListQuery, client: ChatClient, environment: Environment = .init()) { self.query = query @@ -27,7 +27,7 @@ public final class ReactionList { // MARK: - Accessing the State /// An observable object representing the current state of the reaction list. - @MainActor public lazy var state: ReactionListState = stateBuilder.build() + @MainActor public var state: ReactionListState { stateBuilder.reuseOrBuild() } /// Fetches the most recent state from the server and updates the local store. /// @@ -68,13 +68,15 @@ public final class ReactionList { } extension ReactionList { - struct Environment { - var reactionListUpdaterBuilder: ( + struct Environment: Sendable { + var reactionListUpdaterBuilder: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient - ) -> ReactionListUpdater = ReactionListUpdater.init + ) -> ReactionListUpdater = { + ReactionListUpdater(database: $0, apiClient: $1) + } - var stateBuilder: @MainActor( + var stateBuilder: @Sendable @MainActor( _ query: ReactionListQuery, _ database: DatabaseContainer ) -> ReactionListState = { @MainActor in diff --git a/Sources/StreamChat/StateLayer/ReactionListState+Observer.swift b/Sources/StreamChat/StateLayer/ReactionListState+Observer.swift index f9d10e758a2..ee244484095 100644 --- a/Sources/StreamChat/StateLayer/ReactionListState+Observer.swift +++ b/Sources/StreamChat/StateLayer/ReactionListState+Observer.swift @@ -18,7 +18,7 @@ extension ReactionListState { } struct Handlers { - let reactionsDidChange: @MainActor(StreamCollection) async -> Void + let reactionsDidChange: @Sendable @MainActor(StreamCollection) async -> Void } func start(with handlers: Handlers) -> StreamCollection { diff --git a/Sources/StreamChat/StateLayer/UserList.swift b/Sources/StreamChat/StateLayer/UserList.swift index e11baafad76..f5c27fe5094 100644 --- a/Sources/StreamChat/StateLayer/UserList.swift +++ b/Sources/StreamChat/StateLayer/UserList.swift @@ -5,9 +5,9 @@ import Foundation /// An object which represents a list of `ChatUser`. -public final class UserList { +public final class UserList: Sendable { private let query: UserListQuery - private let stateBuilder: StateBuilder + @MainActor private var stateBuilder: StateBuilder private let userListUpdater: UserListUpdater init(query: UserListQuery, client: ChatClient, environment: Environment = .init()) { @@ -27,7 +27,7 @@ public final class UserList { // MARK: - Accessing the State /// An observable object representing the current state of the users list. - @MainActor public lazy var state: UserListState = stateBuilder.build() + @MainActor public var state: UserListState { stateBuilder.reuseOrBuild() } /// Fetches the most recent state from the server and updates the local store. /// @@ -72,13 +72,15 @@ public final class UserList { } extension UserList { - struct Environment { - var userListUpdater: ( + struct Environment: Sendable { + var userListUpdater: @Sendable( _ database: DatabaseContainer, _ apiClient: APIClient - ) -> UserListUpdater = UserListUpdater.init + ) -> UserListUpdater = { + UserListUpdater(database: $0, apiClient: $1) + } - var stateBuilder: @MainActor( + var stateBuilder: @Sendable @MainActor( _ query: UserListQuery, _ database: DatabaseContainer ) -> UserListState = { @MainActor in diff --git a/Sources/StreamChat/StateLayer/UserListState+Observer.swift b/Sources/StreamChat/StateLayer/UserListState+Observer.swift index ea39b21ccfb..9fe7b0ce7e9 100644 --- a/Sources/StreamChat/StateLayer/UserListState+Observer.swift +++ b/Sources/StreamChat/StateLayer/UserListState+Observer.swift @@ -20,7 +20,7 @@ extension UserListState { } struct Handlers { - let usersDidChange: (StreamCollection) async -> Void + let usersDidChange: @Sendable @MainActor(StreamCollection) async -> Void } func start(with handlers: Handlers) -> StreamCollection { diff --git a/Sources/StreamChat/StateLayer/UserSearch.swift b/Sources/StreamChat/StateLayer/UserSearch.swift index 27a77af802f..c7ce6987c92 100644 --- a/Sources/StreamChat/StateLayer/UserSearch.swift +++ b/Sources/StreamChat/StateLayer/UserSearch.swift @@ -5,8 +5,8 @@ import Foundation /// An object which represents a list of `ChatUser` for the specified search query. -public class UserSearch { - private let stateBuilder: StateBuilder +public class UserSearch: @unchecked Sendable { + @MainActor private var stateBuilder: StateBuilder private let userListUpdater: UserListUpdater init(client: ChatClient, environment: Environment = .init()) { @@ -20,7 +20,7 @@ public class UserSearch { // MARK: - Accessing the State /// An observable object representing the current state of the search. - @MainActor public lazy var state: UserSearchState = stateBuilder.build() + @MainActor public var state: UserSearchState { stateBuilder.reuseOrBuild() } // MARK: - Search Results and Pagination diff --git a/Sources/StreamChat/Utils/AllocatedUnfairLock.swift b/Sources/StreamChat/Utils/AllocatedUnfairLock.swift new file mode 100644 index 00000000000..550ab609aae --- /dev/null +++ b/Sources/StreamChat/Utils/AllocatedUnfairLock.swift @@ -0,0 +1,39 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation +import os + +@available(iOS, introduced: 13.0, deprecated: 16.0, message: "Use OSAllocatedUnfairLock instead") +final class AllocatedUnfairLock: @unchecked Sendable { + private let lock: UnsafeMutablePointer + nonisolated(unsafe) private var _value: State + + init(_ initialState: State) { + lock = UnsafeMutablePointer.allocate(capacity: 1) + lock.initialize(to: os_unfair_lock()) + _value = initialState + } + + deinit { + lock.deinitialize(count: 1) + lock.deallocate() + } + + @discardableResult + func withLock(_ body: (inout State) throws -> R) rethrows -> R { + os_unfair_lock_lock(lock) + defer { os_unfair_lock_unlock(lock) } + return try body(&_value) + } + + var value: State { + get { + withLock { $0 } + } + set { + withLock { $0 = newValue } + } + } +} diff --git a/Sources/StreamChat/Utils/Atomic.swift b/Sources/StreamChat/Utils/Atomic.swift index 70422f64e65..cd8f93a4527 100644 --- a/Sources/StreamChat/Utils/Atomic.swift +++ b/Sources/StreamChat/Utils/Atomic.swift @@ -23,7 +23,7 @@ import Foundation /// itself from multiple threads can cause a crash. @propertyWrapper -public class Atomic { +public class Atomic: @unchecked Sendable { public var wrappedValue: T { get { var currentValue: T! diff --git a/Sources/StreamChat/Utils/BoxedAny.swift b/Sources/StreamChat/Utils/BoxedAny.swift new file mode 100644 index 00000000000..0f26b417f01 --- /dev/null +++ b/Sources/StreamChat/Utils/BoxedAny.swift @@ -0,0 +1,29 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// Erase type for structs which recursively contain themselves. +/// +/// Example: +/// ```swift +/// struct ChatMessage { +/// let quotedMessage: ChatMessage? +/// } +/// ``` +/// Can be written as: +/// ```swift +/// struct ChatMessage { +/// let quotedMessage: ChatMessage? { _quotedMessage.value as? ChatMessage } +/// let _quotedMessage: BoxedAny? +/// } +/// ``` +struct BoxedAny { + init?(_ value: (any Sendable)?) { + guard value != nil else { return nil } + self.value = value + } + + let value: any Sendable +} diff --git a/Sources/StreamChat/Utils/Codable+Extensions.swift b/Sources/StreamChat/Utils/Codable+Extensions.swift index 7c26d9edb46..be11edd8acc 100644 --- a/Sources/StreamChat/Utils/Codable+Extensions.swift +++ b/Sources/StreamChat/Utils/Codable+Extensions.swift @@ -202,10 +202,10 @@ extension ISO8601DateFormatter { // MARK: - Helper AnyEncodable -struct AnyEncodable: Encodable { - let encodable: Encodable +struct AnyEncodable: Encodable, Sendable { + let encodable: (Encodable & Sendable) - init(_ encodable: Encodable) { + init(_ encodable: Encodable & Sendable) { self.encodable = encodable } @@ -215,11 +215,13 @@ struct AnyEncodable: Encodable { } } -extension Encodable { +extension Encodable where Self: Sendable { var asAnyEncodable: AnyEncodable { AnyEncodable(self) } +} +extension Encodable { // We need this helper in order to encode AnyEncodable with a singleValueContainer, // this is needed for the encoder to apply the encoding strategies of the inner type (encodable). // More details about this in the following thread: diff --git a/Sources/StreamChat/Utils/CountdownTracker.swift b/Sources/StreamChat/Utils/CountdownTracker.swift index 6fe74c152f8..8432b674d96 100644 --- a/Sources/StreamChat/Utils/CountdownTracker.swift +++ b/Sources/StreamChat/Utils/CountdownTracker.swift @@ -4,9 +4,10 @@ import Foundation -public class CooldownTracker { +public class CooldownTracker: @unchecked Sendable { private var timer: StreamTimer - + @Atomic private var remainingDuration = 0 + public var onChange: ((Int) -> Void)? public init(timer: StreamTimer) { @@ -15,28 +16,30 @@ public class CooldownTracker { public func start(with cooldown: Int) { guard cooldown > 0 else { return } - - var duration = cooldown - + remainingDuration = cooldown + timer.onChange = { [weak self] in - self?.onChange?(duration) + guard let self else { return } + onChange?(remainingDuration) - if duration == 0 { - self?.timer.stop() + if remainingDuration == 0 { + self.timer.stop() } else { - duration -= 1 + _remainingDuration.mutate { value in + value -= 1 + } } } - + timer.start() } - + public func stop() { guard timer.isRunning else { return } timer.stop() } - + deinit { stop() } diff --git a/Sources/StreamChat/Utils/Data+Gzip.swift b/Sources/StreamChat/Utils/Data+Gzip.swift index 9674a22ee4a..c2ad118c897 100644 --- a/Sources/StreamChat/Utils/Data+Gzip.swift +++ b/Sources/StreamChat/Utils/Data+Gzip.swift @@ -150,7 +150,7 @@ struct GzipError: Swift.Error { init(code: Int32, msg: UnsafePointer?) { message = { - guard let msg = msg, let message = String(validatingUTF8: msg) else { + guard let msg = msg, let message = String(validatingCString: msg) else { return "Unknown gzip error" } return message diff --git a/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift b/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift index 77b3e29eaac..5a2b921c366 100644 --- a/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift +++ b/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift @@ -79,7 +79,8 @@ extension NSManagedObject { } static func load(by request: NSFetchRequest, context: NSManagedObjectContext) -> [T] { - request.entity = NSEntityDescription.entity(forEntityName: T.entityName, in: context)! + let entityName = request.entityName ?? T.entityName + request.entity = NSEntityDescription.entity(forEntityName: entityName, in: context)! do { return try context.fetch(request, using: FetchCache.shared) } catch { @@ -94,11 +95,11 @@ extension NSManagedObject { } } -class FetchCache { +final class FetchCache: @unchecked Sendable { /// We use this wrapper to have a custom implementation of both Equatable and Hashable. /// This is because when using NSFetchRequest directly, its implementation of `hash` uses `entity`, which is a property that crashes on access /// when it is not yet set - struct FetchRequestWrapper: Equatable, Hashable { + struct FetchRequestWrapper: Equatable, Hashable, @unchecked Sendable { let request: NSFetchRequest static func == (lhs: FetchRequestWrapper, rhs: FetchRequestWrapper) -> Bool { diff --git a/Sources/StreamChat/Utils/EventBatcher.swift b/Sources/StreamChat/Utils/EventBatcher.swift index 5b95daeedc9..3edc472317f 100644 --- a/Sources/StreamChat/Utils/EventBatcher.swift +++ b/Sources/StreamChat/Utils/EventBatcher.swift @@ -5,9 +5,9 @@ import Foundation /// The type that does events batching. -protocol EventBatcher { +protocol EventBatcher: Sendable { typealias Batch = [Event] - typealias BatchHandler = (_ batch: Batch, _ completion: @escaping () -> Void) -> Void + typealias BatchHandler = (_ batch: Batch, _ completion: @escaping @Sendable() -> Void) -> Void /// The current batch of events. var currentBatch: Batch { get } @@ -22,10 +22,11 @@ protocol EventBatcher { func append(_ event: Event) /// Ignores `period` and passes the current batch of events to handler as soon as possible. - func processImmediately(completion: @escaping () -> Void) + func processImmediately(completion: @escaping @Sendable() -> Void) } extension Batcher: EventBatcher where Item == Event {} +extension Batcher: Sendable where Item == Event {} final class Batcher { /// The batching period. If the item is added sonner then `period` has passed after the first item they will get into the same batch. @@ -33,18 +34,18 @@ final class Batcher { /// The time used to create timers. private let timerType: Timer.Type /// The timer that calls `processor` when fired. - private var batchProcessingTimer: TimerControl? + nonisolated(unsafe) private var batchProcessingTimer: TimerControl? /// The closure which processes the batch. - private let handler: (_ batch: [Item], _ completion: @escaping () -> Void) -> Void + nonisolated(unsafe) private let handler: (_ batch: [Item], _ completion: @escaping @Sendable() -> Void) -> Void /// The serial queue where item appends and batch processing is happening on. private let queue = DispatchQueue(label: "io.getstream.Batch.\(Item.self)") /// The current batch of items. - private(set) var currentBatch: [Item] = [] + nonisolated(unsafe) private(set) var currentBatch: [Item] = [] init( period: TimeInterval, timerType: Timer.Type = DefaultTimer.self, - handler: @escaping (_ batch: [Item], _ completion: @escaping () -> Void) -> Void + handler: @escaping (_ batch: [Item], _ completion: @escaping @Sendable() -> Void) -> Void ) { self.period = max(period, 0) self.timerType = timerType @@ -65,13 +66,13 @@ final class Batcher { } } - func processImmediately(completion: @escaping () -> Void) { + func processImmediately(completion: @escaping @Sendable() -> Void) { timerType.schedule(timeInterval: 0, queue: queue) { [weak self] in self?.process(completion: completion) } } - private func process(completion: (() -> Void)? = nil) { + private func process(completion: (@Sendable() -> Void)? = nil) { handler(currentBatch) { completion?() } currentBatch.removeAll() batchProcessingTimer?.cancel() diff --git a/Sources/StreamChat/Utils/InternetConnection/InternetConnection.swift b/Sources/StreamChat/Utils/InternetConnection/InternetConnection.swift index 7ebaeed4d66..37d7ee3c995 100644 --- a/Sources/StreamChat/Utils/InternetConnection/InternetConnection.swift +++ b/Sources/StreamChat/Utils/InternetConnection/InternetConnection.swift @@ -22,7 +22,7 @@ extension Notification { } /// An Internet Connection monitor. -class InternetConnection { +class InternetConnection: @unchecked Sendable { /// The current Internet connection status. private(set) var status: InternetConnection.Status { didSet { @@ -89,7 +89,7 @@ protocol InternetConnectionDelegate: AnyObject { } /// A protocol for Internet connection monitors. -protocol InternetConnectionMonitor: AnyObject { +protocol InternetConnectionMonitor: AnyObject, Sendable { /// A delegate for receiving Internet connection events. var delegate: InternetConnectionDelegate? { get set } @@ -145,7 +145,7 @@ extension InternetConnection.Status { // MARK: - Internet Connection Monitor extension InternetConnection { - class Monitor: InternetConnectionMonitor { + final class Monitor: InternetConnectionMonitor, @unchecked Sendable { private var monitor: NWPathMonitor? private let queue = DispatchQueue(label: "io.getstream.internet-monitor") @@ -153,7 +153,7 @@ extension InternetConnection { var status: InternetConnection.Status { if let path = monitor?.currentPath { - return status(from: path) + return Self.status(from: path) } return .unknown @@ -176,18 +176,14 @@ extension InternetConnection { // We should be able to do `[weak self]` here, but it seems `NWPathMonitor` sometimes calls the handler // event after `cancel()` has been called on it. - monitor.pathUpdateHandler = { [weak self] in - self?.updateStatus(with: $0) + monitor.pathUpdateHandler = { [weak self] path in + log.info("Internet Connection info: \(path.debugDescription)") + self?.delegate?.internetConnectionStatusDidChange(status: Self.status(from: path)) } return monitor } - - private func updateStatus(with path: NWPath) { - log.info("Internet Connection info: \(path.debugDescription)") - delegate?.internetConnectionStatusDidChange(status: status(from: path)) - } - - private func status(from path: NWPath) -> InternetConnection.Status { + + private static func status(from path: NWPath) -> InternetConnection.Status { guard path.status == .satisfied else { return .unavailable } diff --git a/Sources/StreamChat/Utils/InternetConnection/Reachability_Vendor.swift b/Sources/StreamChat/Utils/InternetConnection/Reachability_Vendor.swift index 5c5fa6685d4..740b89ec420 100644 --- a/Sources/StreamChat/Utils/InternetConnection/Reachability_Vendor.swift +++ b/Sources/StreamChat/Utils/InternetConnection/Reachability_Vendor.swift @@ -13,7 +13,7 @@ enum ReachabilityError: Error { case unableToGetFlags(Int32) } -class Reachability { +class Reachability: @unchecked Sendable { typealias NetworkReachable = (Reachability) -> Void typealias NetworkUnreachable = (Reachability) -> Void @@ -55,7 +55,12 @@ class Reachability { #endif }() - private(set) var notifierRunning = false + private(set) var notifierRunning: Bool { + get { reachabilitySerialQueue.sync { _notifierRunning } } + set { reachabilitySerialQueue.sync { _notifierRunning = newValue } } + } + + private var _notifierRunning = false private let reachabilityRef: SCNetworkReachability private let reachabilitySerialQueue: DispatchQueue private let notificationQueue: DispatchQueue? @@ -209,7 +214,7 @@ private extension Reachability { } func notifyReachabilityChanged() { - let notify = { [weak self] in + let notify: @Sendable() -> Void = { [weak self] in guard let self = self else { return } self.connection != .unavailable ? self.whenReachable?(self) : self.whenUnreachable?(self) } diff --git a/Sources/StreamChat/Utils/Logger/Destination/BaseLogDestination.swift b/Sources/StreamChat/Utils/Logger/Destination/BaseLogDestination.swift index 78743211e60..6575840d695 100644 --- a/Sources/StreamChat/Utils/Logger/Destination/BaseLogDestination.swift +++ b/Sources/StreamChat/Utils/Logger/Destination/BaseLogDestination.swift @@ -6,7 +6,7 @@ import Foundation /// Base class for log destinations. Already implements basic functionality to allow easy destination implementation. /// Extending this class, instead of implementing `LogDestination` is easier (and recommended) for creating new destinations. -open class BaseLogDestination: LogDestination { +open class BaseLogDestination: LogDestination, @unchecked Sendable { open var identifier: String open var level: LogLevel open var subsystems: LogSubsystem diff --git a/Sources/StreamChat/Utils/Logger/Destination/ConsoleLogDestination.swift b/Sources/StreamChat/Utils/Logger/Destination/ConsoleLogDestination.swift index 7d25f70c1f4..d086db2a232 100644 --- a/Sources/StreamChat/Utils/Logger/Destination/ConsoleLogDestination.swift +++ b/Sources/StreamChat/Utils/Logger/Destination/ConsoleLogDestination.swift @@ -5,8 +5,8 @@ import Foundation /// Basic destination for outputting messages to console. -public class ConsoleLogDestination: BaseLogDestination { - override open func write(message: String) { +public final class ConsoleLogDestination: BaseLogDestination, @unchecked Sendable { + override public func write(message: String) { print(message) } } diff --git a/Sources/StreamChat/Utils/Logger/Destination/LogDestination.swift b/Sources/StreamChat/Utils/Logger/Destination/LogDestination.swift index 9b2dd406c59..fcd723f9252 100644 --- a/Sources/StreamChat/Utils/Logger/Destination/LogDestination.swift +++ b/Sources/StreamChat/Utils/Logger/Destination/LogDestination.swift @@ -6,7 +6,7 @@ import Foundation /// Log level for any messages to be logged. /// Please check [this Apple Logging Article](https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code) to understand different logging levels. -public enum LogLevel: Int { +public enum LogLevel: Int, Sendable { /// Use this log level if you want to see everything that is logged. case debug = 0 /// Use this log level if you want to see what is happening during the app execution. @@ -31,7 +31,7 @@ public struct LogDetails { public let lineNumber: UInt } -public protocol LogDestination { +public protocol LogDestination: Sendable { var identifier: String { get set } var level: LogLevel { get set } var subsystems: LogSubsystem { get set } diff --git a/Sources/StreamChat/Utils/Logger/Logger.swift b/Sources/StreamChat/Utils/Logger/Logger.swift index 644816032a6..ee949e14761 100644 --- a/Sources/StreamChat/Utils/Logger/Logger.swift +++ b/Sources/StreamChat/Utils/Logger/Logger.swift @@ -9,7 +9,7 @@ public var log: Logger { } /// Entity for identifying which subsystem the log message comes from. -public struct LogSubsystem: OptionSet { +public struct LogSubsystem: OptionSet, Sendable { public let rawValue: Int public init(rawValue: Int) { @@ -41,170 +41,253 @@ public struct LogSubsystem: OptionSet { public enum LogConfig { /// Identifier for the logger. Defaults to empty. - public static var identifier = "" { - didSet { + public static var identifier: String { + get { + queue.sync { _storage.identifier } + } + set { + queue.async { _storage.identifier = newValue } invalidateLogger() } } - /// Output level for the logger. - public static var level: LogLevel = .error { - didSet { + /// Output level for the logger. Defaults to error. + public static var level: LogLevel { + get { + queue.sync { _storage.level } + } + set { + queue.async { _storage.level = newValue } invalidateLogger() } } /// Date formatter for the logger. Defaults to ISO8601 - public static var dateFormatter: DateFormatter = { - let df = DateFormatter() - df.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" - return df - }() { - didSet { + public static var dateFormatter: DateFormatter { + get { + queue.sync { _storage.dateFormatter } + } + set { + queue.async { _storage.dateFormatter = newValue } invalidateLogger() } } /// Log formatters to be applied in order before logs are outputted. Defaults to empty (no formatters). /// Please see `LogFormatter` for more info. - public static var formatters = [LogFormatter]() { - didSet { + public static var formatters: [LogFormatter] { + get { + queue.sync { _storage.formatters } + } + set { + queue.sync { _storage.formatters = newValue } invalidateLogger() } } - /// Toggle for showing date in logs - public static var showDate = true { - didSet { + /// Toggle for showing date in logs. Defaults to true. + public static var showDate: Bool { + get { + queue.sync { _storage.showDate } + } + set { + queue.async { _storage.showDate = newValue } invalidateLogger() } } - /// Toggle for showing log level in logs - public static var showLevel = true { - didSet { + /// Toggle for showing log level in logs. Defaults to true. + public static var showLevel: Bool { + get { + queue.sync { _storage.showLevel } + } + set { + queue.async { _storage.showLevel = newValue } invalidateLogger() } } - /// Toggle for showing identifier in logs - public static var showIdentifier = false { - didSet { + /// Toggle for showing identifier in logs. Defaults to false. + public static var showIdentifier: Bool { + get { + queue.sync { _storage.showIdentifier } + } + set { + queue.async { _storage.showIdentifier = newValue } invalidateLogger() } } - /// Toggle for showing thread name in logs - public static var showThreadName = true { - didSet { + /// Toggle for showing thread name in logs. Defaults to true. + public static var showThreadName: Bool { + get { + queue.sync { _storage.showThreadName } + } + set { + queue.async { _storage.showThreadName = newValue } invalidateLogger() } } - /// Toggle for showing file name in logs - public static var showFileName = true { - didSet { + /// Toggle for showing file name in logs. Defaults to true. + public static var showFileName: Bool { + get { + queue.sync { _storage.showFileName } + } + set { + queue.async { _storage.showFileName = newValue } invalidateLogger() } } - /// Toggle for showing line number in logs - public static var showLineNumber = true { - didSet { + /// Toggle for showing line number in logs. Defaults to true. + public static var showLineNumber: Bool { + get { + queue.sync { _storage.showLineNumber } + } + set { + queue.async { _storage.showLineNumber = newValue } invalidateLogger() } } - /// Toggle for showing function name in logs - public static var showFunctionName = true { - didSet { + /// Toggle for showing function name in logs. Defaults to true. + public static var showFunctionName: Bool { + get { + queue.sync { _storage.showFunctionName } + } + set { + queue.async { _storage.showFunctionName = newValue } invalidateLogger() } } - /// Subsystems for the logger - public static var subsystems: LogSubsystem = .all { - didSet { + /// Subsystems for the logger. Defaults to ``LogSubsystem.all``. + public static var subsystems: LogSubsystem { + get { + queue.sync { _storage.subsystems } + } + set { + queue.async { _storage.subsystems = newValue } invalidateLogger() } } - /// Destination types this logger will use. + /// Destination types this logger will use. Defaults to ``ConsoleLogDestination``. /// /// Logger will initialize the destinations with its own parameters. If you want full control on the parameters, use `destinations` directly, /// where you can pass parameters to destination initializers yourself. - public static var destinationTypes: [LogDestination.Type] = [ConsoleLogDestination.self] { - didSet { + public static var destinationTypes: [LogDestination.Type] { + get { + queue.sync { _storage.destinationTypes } + } + set { + queue.async { _storage.destinationTypes = newValue } invalidateLogger() } } - private static var _destinations: [LogDestination]? - /// Destinations for the default logger. Please see `LogDestination`. /// Defaults to only `ConsoleLogDestination`, which only prints the messages. /// /// - Important: Other options in `ChatClientConfig.Logging` will not take affect if this is changed. public static var destinations: [LogDestination] { get { - if let destinations = _destinations { - return destinations - } else { - _destinations = destinationTypes.map { - $0.init( - identifier: identifier, - level: level, - subsystems: subsystems, - showDate: showDate, - dateFormatter: dateFormatter, - formatters: formatters, - showLevel: showLevel, - showIdentifier: showIdentifier, - showThreadName: showThreadName, - showFileName: showFileName, - showLineNumber: showLineNumber, - showFunctionName: showFunctionName - ) + queue.sync { + if let destinations = _storage.destinations { + return destinations + } else { + return _setDefaultDestinationsIfNeeded() } - return _destinations! } } set { invalidateLogger() - _destinations = newValue + queue.async { _storage.destinations = newValue } } } + + private static func _setDefaultDestinationsIfNeeded() -> [LogDestination] { + if let destinations = _storage.destinations { + return destinations + } + _storage.destinations = _storage.destinationTypes.map { + $0.init( + identifier: _storage.identifier, + level: _storage.level, + subsystems: _storage.subsystems, + showDate: _storage.showDate, + dateFormatter: _storage.dateFormatter, + formatters: _storage.formatters, + showLevel: _storage.showLevel, + showIdentifier: _storage.showIdentifier, + showThreadName: _storage.showThreadName, + showFileName: _storage.showFileName, + showLineNumber: _storage.showLineNumber, + showFunctionName: _storage.showFunctionName + ) + } + return _storage.destinations! + } - /// Underlying logger instance to control singleton. - private static var _logger: Logger? - - private static var queue = DispatchQueue(label: "io.getstream.logconfig") - + private static let queue = DispatchQueue(label: "io.getstream.logconfig") + + /// Guarded manually with a queue + nonisolated(unsafe) private static var _storage: Storage = Storage() + /// Logger instance to be used by StreamChat. /// /// - Important: Other options in `LogConfig` will not take affect if this is changed. public static var logger: Logger { get { queue.sync { - if let logger = _logger { + if let logger = _storage.logger { return logger } else { - _logger = Logger(identifier: identifier, destinations: destinations) - return _logger! + let destinations = _setDefaultDestinationsIfNeeded() + let logger = Logger(identifier: _storage.identifier, destinations: destinations) + _storage.logger = logger + return logger } } } set { - queue.async { - _logger = newValue - } + queue.sync { _storage.logger = newValue } } } /// Invalidates the current logger instance so it can be recreated. private static func invalidateLogger() { - _logger = nil - _destinations = nil + queue.async { + _storage.logger = nil + _storage.destinations = nil + } + } +} + +extension LogConfig { + struct Storage { + init() { + dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" + } + + var logger: Logger? + + var identifier: String = "" + var level: LogLevel = .error + var dateFormatter: DateFormatter + var formatters = [LogFormatter]() + var showDate = true + var showLevel = true + var showIdentifier = false + var showThreadName = true + var showFileName = true + var showLineNumber = true + var showFunctionName = true + var subsystems: LogSubsystem = .all + var destinationTypes: [LogDestination.Type] = [ConsoleLogDestination.self] + var destinations: [LogDestination]? } } @@ -250,7 +333,7 @@ public class Logger { public func callAsFunction( _ level: LogLevel, functionName: StaticString = #function, - fileName: StaticString = #filePath, + fileName: StaticString = #file, lineNumber: UInt = #line, message: @autoclosure () -> Any, subsystems: LogSubsystem = .other @@ -463,6 +546,9 @@ public class Logger { } } +// Mutable state is guarded with a queue. +extension Logger: @unchecked Sendable {} + private extension Logger { var threadName: String { if Thread.isMainThread { @@ -470,7 +556,7 @@ private extension Logger { } else { if let threadName = Thread.current.name, !threadName.isEmpty { return "[\(threadName)] " - } else if let queueName = String(validatingUTF8: __dispatch_queue_get_label(nil)), !queueName.isEmpty { + } else if let queueName = String(validatingCString: __dispatch_queue_get_label(nil)), !queueName.isEmpty { return "[\(queueName)] " } else { return String(format: "[%p] ", Thread.current) diff --git a/Sources/StreamChat/Utils/MainQueue+Synchronous.swift b/Sources/StreamChat/Utils/MainQueue+Synchronous.swift index 0b68b2b3247..e05828e7bd7 100644 --- a/Sources/StreamChat/Utils/MainQueue+Synchronous.swift +++ b/Sources/StreamChat/Utils/MainQueue+Synchronous.swift @@ -4,19 +4,23 @@ import Foundation -extension DispatchQueue { +extension MainActor { /// Synchronously performs the provided action on the main thread. /// - /// Performing this action is safe because the function checks the current thread, and if it's currently in the main - /// one, it performs the action safely without dead-locking the thread. - /// - static func performSynchronouslyOnMainQueue(_ action: () throws -> Void) rethrows { + /// Used for ensuring we are on the main thread when compiler can't know it. For example, + /// controller completion handlers by default are called from main thread, but one can + /// configure controller to use background thread for completions instead. + static func ensureIsolated(_ action: @MainActor @Sendable() throws -> T) rethrows -> T where T: Sendable { if Thread.current.isMainThread { - try action() - } else { - try DispatchQueue.main.sync { + return try MainActor.assumeIsolated { try action() } + } else { + return try DispatchQueue.main.sync { + return try MainActor.assumeIsolated { + try action() + } + } } } } diff --git a/Sources/StreamChat/Utils/MessagesPaginationStateHandling/MessagesPaginationState.swift b/Sources/StreamChat/Utils/MessagesPaginationStateHandling/MessagesPaginationState.swift index 27cb790cbe0..3f59a6f2a69 100644 --- a/Sources/StreamChat/Utils/MessagesPaginationStateHandling/MessagesPaginationState.swift +++ b/Sources/StreamChat/Utils/MessagesPaginationStateHandling/MessagesPaginationState.swift @@ -5,7 +5,7 @@ import Foundation /// The current state of the messages pagination. -struct MessagesPaginationState { +struct MessagesPaginationState: Sendable { // MARK: State /// The oldest fetched message while paginating. diff --git a/Sources/StreamChat/Utils/MessagesPaginationStateHandling/MessagesPaginationStateHandling.swift b/Sources/StreamChat/Utils/MessagesPaginationStateHandling/MessagesPaginationStateHandling.swift index 179fd6553a9..840caff4ead 100644 --- a/Sources/StreamChat/Utils/MessagesPaginationStateHandling/MessagesPaginationStateHandling.swift +++ b/Sources/StreamChat/Utils/MessagesPaginationStateHandling/MessagesPaginationStateHandling.swift @@ -5,7 +5,7 @@ import Foundation /// A component responsible for handling the messages pagination state. -protocol MessagesPaginationStateHandling { +protocol MessagesPaginationStateHandling: Sendable { /// The current state of the messages pagination. var state: MessagesPaginationState { get } @@ -17,7 +17,7 @@ protocol MessagesPaginationStateHandling { } /// A component responsible for handling the messages pagination state. -class MessagesPaginationStateHandler: MessagesPaginationStateHandling { +class MessagesPaginationStateHandler: MessagesPaginationStateHandling, @unchecked Sendable { private let queue = DispatchQueue(label: "io.getstream.messages-pagination-state-handler") private var _state: MessagesPaginationState = .initial diff --git a/Sources/StreamChat/Utils/Operations/AsyncOperation.swift b/Sources/StreamChat/Utils/Operations/AsyncOperation.swift index d250d0f7be9..ca841c51035 100644 --- a/Sources/StreamChat/Utils/Operations/AsyncOperation.swift +++ b/Sources/StreamChat/Utils/Operations/AsyncOperation.swift @@ -11,14 +11,14 @@ class AsyncOperation: BaseOperation, @unchecked Sendable { } private let maxRetries: Int - private(set) var executionBlock: (AsyncOperation, @escaping (_ output: Output) -> Void) -> Void + private(set) var executionBlock: (AsyncOperation, @escaping @Sendable(_ output: Output) -> Void) -> Void private var executedRetries = 0 var canRetry: Bool { executedRetries < maxRetries && !isCancelled && !isFinished } - init(maxRetries: Int = 0, executionBlock: @escaping (AsyncOperation, @escaping (_ output: Output) -> Void) -> Void) { + init(maxRetries: Int = 0, executionBlock: @escaping (AsyncOperation, @escaping @Sendable(_ output: Output) -> Void) -> Void) { self.maxRetries = maxRetries self.executionBlock = executionBlock super.init() diff --git a/Sources/StreamChat/Utils/StateBuilder.swift b/Sources/StreamChat/Utils/StateBuilder.swift index 17dafc26616..40f1f60cf3f 100644 --- a/Sources/StreamChat/Utils/StateBuilder.swift +++ b/Sources/StreamChat/Utils/StateBuilder.swift @@ -5,14 +5,20 @@ import Foundation /// A builder for objects requiring @MainActor. -struct StateBuilder { - private let builder: (@MainActor() -> State) +struct StateBuilder: Sendable { + private var builder: ((@Sendable @MainActor() -> State))? + @MainActor private var _state: State? - init(builder: (@escaping @MainActor() -> State)) { + init(builder: (@escaping @Sendable @MainActor() -> State)) { self.builder = builder } - @MainActor func build() -> State { - builder() + @MainActor mutating func reuseOrBuild() -> State { + if let _state { return _state } + let state = builder!() + _state = state + // Release captured values in the closure + builder = nil + return state } } diff --git a/Sources/StreamChat/Utils/StreamCollection.swift b/Sources/StreamChat/Utils/StreamCollection.swift index 62b739cc923..23f46da2ae8 100644 --- a/Sources/StreamChat/Utils/StreamCollection.swift +++ b/Sources/StreamChat/Utils/StreamCollection.swift @@ -9,30 +9,27 @@ import Foundation /// - Note: The type of the base collection can change in the future. public struct StreamCollection: RandomAccessCollection { public typealias Index = Int - - private let _endIndex: () -> Index - private let _position: (Index) -> Element - private let _startIndex: () -> Index - + private let baseCollection: [Element] + /// Creates an instance of the collection using the base collection as the data source. public init(_ baseCollection: BaseCollection) where BaseCollection: RandomAccessCollection, BaseCollection.Element == Element, BaseCollection.Index == Index { - _endIndex = { baseCollection.endIndex } - _position = { baseCollection[$0] } - _startIndex = { baseCollection.startIndex } + self.baseCollection = Array(baseCollection) } /// The position of the first element in a non-empty collection. - public var startIndex: Index { _startIndex() } + public var startIndex: Index { baseCollection.startIndex } /// The collection's “past the end” position—that is, the position one greater than the last valid subscript argument. - public var endIndex: Index { _endIndex() } + public var endIndex: Index { baseCollection.endIndex } /// Accesses the element at the specified position. public subscript(position: Index) -> Element { - _position(position) + baseCollection[position] } } +extension StreamCollection: Sendable where Element: Sendable {} + extension StreamCollection: CustomStringConvertible { public var description: String { let contents = map { String(describing: $0) }.joined(separator: ", ") diff --git a/Sources/StreamChat/Utils/StreamTimer/ScheduledStreamTimer.swift b/Sources/StreamChat/Utils/StreamTimer/ScheduledStreamTimer.swift index ecbe8ffc688..2c38a4d9cdc 100644 --- a/Sources/StreamChat/Utils/StreamTimer/ScheduledStreamTimer.swift +++ b/Sources/StreamChat/Utils/StreamTimer/ScheduledStreamTimer.swift @@ -4,10 +4,10 @@ import Foundation -public class ScheduledStreamTimer: StreamTimer { +public class ScheduledStreamTimer: StreamTimer, @unchecked Sendable { var runLoop = RunLoop.current var timer: Foundation.Timer? - public var onChange: (() -> Void)? + public var onChange: (@Sendable() -> Void)? let interval: TimeInterval let fireOnStart: Bool diff --git a/Sources/StreamChat/Utils/StreamTimer/StreamTimer.swift b/Sources/StreamChat/Utils/StreamTimer/StreamTimer.swift index d5bf97d77ab..daa6e16051f 100644 --- a/Sources/StreamChat/Utils/StreamTimer/StreamTimer.swift +++ b/Sources/StreamChat/Utils/StreamTimer/StreamTimer.swift @@ -7,6 +7,6 @@ import Foundation public protocol StreamTimer { func start() func stop() - var onChange: (() -> Void)? { get set } + var onChange: (@Sendable() -> Void)? { get set } var isRunning: Bool { get } } diff --git a/Sources/StreamChat/Utils/SystemEnvironment+XStreamClient.swift b/Sources/StreamChat/Utils/SystemEnvironment+XStreamClient.swift index ffa403065e8..fde1267771c 100644 --- a/Sources/StreamChat/Utils/SystemEnvironment+XStreamClient.swift +++ b/Sources/StreamChat/Utils/SystemEnvironment+XStreamClient.swift @@ -63,7 +63,12 @@ extension SystemEnvironment { private static var osVersion: String { #if os(iOS) - return UIDevice.current.systemVersion + let version = ProcessInfo.processInfo.operatingSystemVersion + let patch: Int? = version.patchVersion > 0 ? version.patchVersion : nil + return [version.majorVersion, version.minorVersion, patch] + .compactMap { $0 } + .compactMap(String.init) + .joined(separator: ".") #elseif os(macOS) return ProcessInfo.processInfo.operatingSystemVersionString #endif diff --git a/Sources/StreamChat/Utils/TextLinkDetector.swift b/Sources/StreamChat/Utils/TextLinkDetector.swift index d0a8b34582d..9a5ce15bb5e 100644 --- a/Sources/StreamChat/Utils/TextLinkDetector.swift +++ b/Sources/StreamChat/Utils/TextLinkDetector.swift @@ -5,7 +5,7 @@ import Foundation /// The link details found in text. -public struct TextLink: Equatable { +public struct TextLink: Equatable, Sendable { /// The url of the link. public let url: URL /// The original text. diff --git a/Sources/StreamChat/Utils/ThreadSafeWeakCollection.swift b/Sources/StreamChat/Utils/ThreadSafeWeakCollection.swift index 19977b74b06..d059893921e 100644 --- a/Sources/StreamChat/Utils/ThreadSafeWeakCollection.swift +++ b/Sources/StreamChat/Utils/ThreadSafeWeakCollection.swift @@ -4,9 +4,9 @@ import Foundation -final class ThreadSafeWeakCollection { +final class ThreadSafeWeakCollection: Sendable { private let queue = DispatchQueue(label: "io.stream.com.weak-collection", attributes: .concurrent) - private let storage = NSHashTable.weakObjects() + nonisolated(unsafe) private let storage = NSHashTable.weakObjects() var allObjects: [T] { var objects: [T]! diff --git a/Sources/StreamChat/Utils/Timers.swift b/Sources/StreamChat/Utils/Timers.swift index 851dd9d9592..74c0929d8ef 100644 --- a/Sources/StreamChat/Utils/Timers.swift +++ b/Sources/StreamChat/Utils/Timers.swift @@ -39,7 +39,7 @@ extension Timer { } /// Allows resuming and suspending of a timer. -protocol RepeatingTimerControl { +protocol RepeatingTimerControl: Sendable { /// Resumes the timer. func resume() @@ -48,12 +48,17 @@ protocol RepeatingTimerControl { } /// Allows cancelling a timer. -protocol TimerControl { +protocol TimerControl: Sendable { /// Cancels the timer. func cancel() } extension DispatchWorkItem: TimerControl {} +#if compiler(>=6.0) +extension DispatchWorkItem: @retroactive @unchecked Sendable {} +#else +extension DispatchWorkItem: @unchecked Sendable {} +#endif /// Default real-world implementations of timers. struct DefaultTimer: Timer { @@ -77,51 +82,51 @@ struct DefaultTimer: Timer { } } -private class RepeatingTimer: RepeatingTimerControl { +private final class RepeatingTimer: RepeatingTimerControl { private enum State { case suspended case resumed } private let queue = DispatchQueue(label: "io.getstream.repeating-timer") - private var state: State = .suspended - private let timer: DispatchSourceTimer + nonisolated(unsafe) private var _state: State = .suspended + private let _timer: DispatchSourceTimer init(timeInterval: TimeInterval, queue: DispatchQueue, onFire: @escaping () -> Void) { - timer = DispatchSource.makeTimerSource(queue: queue) - timer.schedule(deadline: .now() + .milliseconds(Int(timeInterval)), repeating: timeInterval, leeway: .seconds(1)) - timer.setEventHandler(handler: onFire) + _timer = DispatchSource.makeTimerSource(queue: queue) + _timer.schedule(deadline: .now() + .milliseconds(Int(timeInterval)), repeating: timeInterval, leeway: .seconds(1)) + _timer.setEventHandler(handler: onFire) } deinit { - timer.setEventHandler {} - timer.cancel() + _timer.setEventHandler {} + _timer.cancel() // If the timer is suspended, calling cancel without resuming // triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902 - if state == .suspended { - timer.resume() + if _state == .suspended { + _timer.resume() } } func resume() { queue.async { - if self.state == .resumed { + if self._state == .resumed { return } - self.state = .resumed - self.timer.resume() + self._state = .resumed + self._timer.resume() } } func suspend() { queue.async { - if self.state == .suspended { + if self._state == .suspended { return } - self.state = .suspended - self.timer.suspend() + self._state = .suspended + self._timer.suspend() } } } diff --git a/Sources/StreamChat/Utils/TranslationLanguage.swift b/Sources/StreamChat/Utils/TranslationLanguage.swift index ad5e386c474..70dfaf81261 100644 --- a/Sources/StreamChat/Utils/TranslationLanguage.swift +++ b/Sources/StreamChat/Utils/TranslationLanguage.swift @@ -4,7 +4,7 @@ import Foundation -public struct TranslationLanguage: Hashable { +public struct TranslationLanguage: Hashable, Sendable { public let languageCode: String public init(languageCode: String) { diff --git a/Sources/StreamChat/WebSocketClient/BackgroundTaskScheduler.swift b/Sources/StreamChat/WebSocketClient/BackgroundTaskScheduler.swift index a203485a4c2..62046103cb6 100644 --- a/Sources/StreamChat/WebSocketClient/BackgroundTaskScheduler.swift +++ b/Sources/StreamChat/WebSocketClient/BackgroundTaskScheduler.swift @@ -5,11 +5,11 @@ import Foundation /// Object responsible for platform specific handling of background tasks -protocol BackgroundTaskScheduler { +protocol BackgroundTaskScheduler: Sendable { /// It's your responsibility to finish previously running task. /// /// Returns: `false` if system forbid background task, `true` otherwise - func beginTask(expirationHandler: (() -> Void)?) -> Bool + func beginTask(expirationHandler: (@Sendable @MainActor() -> Void)?) -> Bool func endTask() func startListeningForAppStateUpdates( onEnteringBackground: @escaping () -> Void, @@ -23,7 +23,7 @@ protocol BackgroundTaskScheduler { #if os(iOS) import UIKit -class IOSBackgroundTaskScheduler: BackgroundTaskScheduler { +class IOSBackgroundTaskScheduler: BackgroundTaskScheduler, @unchecked Sendable { private lazy var app: UIApplication? = { // We can't use `UIApplication.shared` directly because there's no way to convince the compiler // this code is accessible only for non-extension executables. @@ -36,10 +36,12 @@ class IOSBackgroundTaskScheduler: BackgroundTaskScheduler { var isAppActive: Bool { if Thread.isMainThread { - return app?.applicationState == .active + return MainActor.assumeIsolated { + app?.applicationState == .active + } } - var isActive = false + nonisolated(unsafe) var isActive = false let group = DispatchGroup() group.enter() DispatchQueue.main.async { @@ -50,7 +52,7 @@ class IOSBackgroundTaskScheduler: BackgroundTaskScheduler { return isActive } - func beginTask(expirationHandler: (() -> Void)?) -> Bool { + func beginTask(expirationHandler: (@Sendable @MainActor() -> Void)?) -> Bool { // Only a single task is allowed at the same time endTask() diff --git a/Sources/StreamChat/WebSocketClient/ConnectionStatus.swift b/Sources/StreamChat/WebSocketClient/ConnectionStatus.swift index cab83c12c46..bb88a5ec361 100644 --- a/Sources/StreamChat/WebSocketClient/ConnectionStatus.swift +++ b/Sources/StreamChat/WebSocketClient/ConnectionStatus.swift @@ -7,7 +7,7 @@ import Foundation // `ConnectionStatus` is just a simplified and friendlier wrapper around `WebSocketConnectionState`. /// Describes the possible states of the client connection to the servers. -public enum ConnectionStatus: Equatable { +public enum ConnectionStatus: Equatable, Sendable { /// The client is initialized but not connected to the remote server yet. case initialized diff --git a/Sources/StreamChat/WebSocketClient/Engine/URLSessionWebSocketEngine.swift b/Sources/StreamChat/WebSocketClient/Engine/URLSessionWebSocketEngine.swift index 7328c3252e6..88e3acbd59d 100644 --- a/Sources/StreamChat/WebSocketClient/Engine/URLSessionWebSocketEngine.swift +++ b/Sources/StreamChat/WebSocketClient/Engine/URLSessionWebSocketEngine.swift @@ -4,7 +4,8 @@ import Foundation -class URLSessionWebSocketEngine: NSObject, WebSocketEngine { +// Note: Synchronization of the instance is handled by WebSocketClient, therefore all the properties are unsafe here. +class URLSessionWebSocketEngine: NSObject, WebSocketEngine, @unchecked Sendable { private weak var task: URLSessionWebSocketTask? { didSet { oldValue?.cancel() @@ -93,7 +94,7 @@ class URLSessionWebSocketEngine: NSObject, WebSocketEngine { private func makeURLSessionDelegateHandler() -> URLSessionDelegateHandler { let urlSessionDelegateHandler = URLSessionDelegateHandler() urlSessionDelegateHandler.onOpen = { [weak self] _ in - self?.callbackQueue.async { + self?.callbackQueue.async { [weak self] in self?.delegate?.webSocketDidConnect() } } @@ -109,7 +110,7 @@ class URLSessionWebSocketEngine: NSObject, WebSocketEngine { ) } - self?.callbackQueue.async { [weak self] in + self?.callbackQueue.async { [weak self, error] in self?.delegate?.webSocketDidDisconnect(error: error) } } @@ -134,7 +135,7 @@ class URLSessionWebSocketEngine: NSObject, WebSocketEngine { } } -final class URLSessionDelegateHandler: NSObject, URLSessionDataDelegate, URLSessionWebSocketDelegate { +final class URLSessionDelegateHandler: NSObject, URLSessionDataDelegate, URLSessionWebSocketDelegate, @unchecked Sendable { var onOpen: ((_ protocol: String?) -> Void)? var onClose: ((_ code: URLSessionWebSocketTask.CloseCode, _ reason: Data?) -> Void)? var onCompletion: ((Error?) -> Void)? diff --git a/Sources/StreamChat/WebSocketClient/Engine/WebSocketEngine.swift b/Sources/StreamChat/WebSocketClient/Engine/WebSocketEngine.swift index d713482dfc1..19683613f17 100644 --- a/Sources/StreamChat/WebSocketClient/Engine/WebSocketEngine.swift +++ b/Sources/StreamChat/WebSocketClient/Engine/WebSocketEngine.swift @@ -4,7 +4,7 @@ import Foundation -protocol WebSocketEngine: AnyObject { +protocol WebSocketEngine: AnyObject, Sendable { var request: URLRequest { get } var callbackQueue: DispatchQueue { get } var delegate: WebSocketEngineDelegate? { get set } diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/UserChannelBanEventsMiddleware.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/UserChannelBanEventsMiddleware.swift index 9ffdf9f4ce3..3a5da148941 100644 --- a/Sources/StreamChat/WebSocketClient/EventMiddlewares/UserChannelBanEventsMiddleware.swift +++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/UserChannelBanEventsMiddleware.swift @@ -42,7 +42,7 @@ struct UserChannelBanEventsMiddleware: EventMiddleware { } extension ClientError { - final class MemberDoesNotExist: ClientError { + final class MemberDoesNotExist: ClientError, @unchecked Sendable { init(userId: UserId, cid: ChannelId) { super.init("There is no `MemberDTO` instance in the DB matching userId: \(userId) and cid: \(cid).") } diff --git a/Sources/StreamChat/WebSocketClient/Events/AITypingEvents.swift b/Sources/StreamChat/WebSocketClient/Events/AITypingEvents.swift index 7db59d3e52a..b1d29d4a7a6 100644 --- a/Sources/StreamChat/WebSocketClient/Events/AITypingEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/AITypingEvents.swift @@ -16,7 +16,7 @@ public struct AIIndicatorUpdateEvent: Event { public let aiMessage: String? } -class AIIndicatorUpdateEventDTO: EventDTO { +final class AIIndicatorUpdateEventDTO: EventDTO { let payload: EventPayload init(from response: EventPayload) throws { @@ -44,7 +44,7 @@ public struct AIIndicatorClearEvent: Event { public let cid: ChannelId? } -class AIIndicatorClearEventDTO: EventDTO { +final class AIIndicatorClearEventDTO: EventDTO { let payload: EventPayload init(from response: EventPayload) throws { @@ -58,7 +58,7 @@ class AIIndicatorClearEventDTO: EventDTO { /// An event that indicates the AI has stopped generating the message. public struct AIIndicatorStopEvent: CustomEventPayload, Event { - public static var eventType: EventType = .aiTypingIndicatorStop + public static let eventType: EventType = .aiTypingIndicatorStop /// The channel ID this event is related to. public let cid: ChannelId? @@ -68,7 +68,7 @@ public struct AIIndicatorStopEvent: CustomEventPayload, Event { } } -class AIIndicatorStopEventDTO: EventDTO { +final class AIIndicatorStopEventDTO: EventDTO { let payload: EventPayload init(from response: EventPayload) throws { @@ -81,7 +81,7 @@ class AIIndicatorStopEventDTO: EventDTO { } /// The state of the AI typing indicator. -public struct AITypingState: ExpressibleByStringLiteral, Hashable { +public struct AITypingState: ExpressibleByStringLiteral, Hashable, Sendable { public var rawValue: String public init?(rawValue: String) { diff --git a/Sources/StreamChat/WebSocketClient/Events/ChannelEvents.swift b/Sources/StreamChat/WebSocketClient/Events/ChannelEvents.swift index bfc09d50077..dda909cc7c2 100644 --- a/Sources/StreamChat/WebSocketClient/Events/ChannelEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/ChannelEvents.swift @@ -22,7 +22,7 @@ public struct ChannelUpdatedEvent: ChannelSpecificEvent { public let createdAt: Date } -class ChannelUpdatedEventDTO: EventDTO { +final class ChannelUpdatedEventDTO: EventDTO { let channel: ChannelDetailPayload let user: UserPayload? let message: MessagePayload? @@ -67,7 +67,7 @@ public struct ChannelDeletedEvent: ChannelSpecificEvent { public let createdAt: Date } -class ChannelDeletedEventDTO: EventDTO { +final class ChannelDeletedEventDTO: EventDTO { let user: UserPayload? let channel: ChannelDetailPayload let createdAt: Date @@ -111,7 +111,7 @@ public struct ChannelTruncatedEvent: ChannelSpecificEvent { public let createdAt: Date } -class ChannelTruncatedEventDTO: EventDTO { +final class ChannelTruncatedEventDTO: EventDTO { let channel: ChannelDetailPayload let user: UserPayload? let createdAt: Date @@ -153,7 +153,7 @@ public struct ChannelVisibleEvent: ChannelSpecificEvent { public let createdAt: Date } -class ChannelVisibleEventDTO: EventDTO { +final class ChannelVisibleEventDTO: EventDTO { let cid: ChannelId let user: UserPayload let createdAt: Date @@ -192,7 +192,7 @@ public struct ChannelHiddenEvent: ChannelSpecificEvent { public let createdAt: Date } -class ChannelHiddenEventDTO: EventDTO { +final class ChannelHiddenEventDTO: EventDTO { let cid: ChannelId let user: UserPayload let isHistoryCleared: Bool diff --git a/Sources/StreamChat/WebSocketClient/Events/ConnectionEvents.swift b/Sources/StreamChat/WebSocketClient/Events/ConnectionEvents.swift index c8fcb3d9983..62866c27b23 100644 --- a/Sources/StreamChat/WebSocketClient/Events/ConnectionEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/ConnectionEvents.swift @@ -8,7 +8,7 @@ public protocol ConnectionEvent: Event { var connectionId: String { get } } -public class HealthCheckEvent: ConnectionEvent, EventDTO { +public final class HealthCheckEvent: ConnectionEvent, EventDTO, Sendable { public let connectionId: String let payload: EventPayload diff --git a/Sources/StreamChat/WebSocketClient/Events/DraftEvents.swift b/Sources/StreamChat/WebSocketClient/Events/DraftEvents.swift index 69263ba6087..1cfae025f10 100644 --- a/Sources/StreamChat/WebSocketClient/Events/DraftEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/DraftEvents.swift @@ -5,7 +5,7 @@ import Foundation /// Triggered when a draft message is updated or created. -public class DraftUpdatedEvent: Event { +public final class DraftUpdatedEvent: Event { /// The channel identifier of the draft. public let cid: ChannelId @@ -26,7 +26,7 @@ public class DraftUpdatedEvent: Event { } } -class DraftUpdatedEventDTO: EventDTO { +final class DraftUpdatedEventDTO: EventDTO { let cid: ChannelId let draft: DraftPayload let createdAt: Date @@ -55,7 +55,7 @@ class DraftUpdatedEventDTO: EventDTO { } /// Triggered when a draft message is deleted. -public class DraftDeletedEvent: Event { +public final class DraftDeletedEvent: Event { /// The channel identifier of the draft. public let cid: ChannelId @@ -72,7 +72,7 @@ public class DraftDeletedEvent: Event { } } -class DraftDeletedEventDTO: EventDTO { +final class DraftDeletedEventDTO: EventDTO { let cid: ChannelId let draft: DraftPayload let createdAt: Date diff --git a/Sources/StreamChat/WebSocketClient/Events/Event.swift b/Sources/StreamChat/WebSocketClient/Events/Event.swift index 90d9cd74e5d..5ceceb2a191 100644 --- a/Sources/StreamChat/WebSocketClient/Events/Event.swift +++ b/Sources/StreamChat/WebSocketClient/Events/Event.swift @@ -5,7 +5,7 @@ import Foundation /// An `Event` object representing an event in the chat system. -public protocol Event {} +public protocol Event: Sendable {} public extension Event { var name: String { @@ -49,7 +49,7 @@ public protocol MemberEvent: Event { } /// A protocol custom event payload must conform to. -public protocol CustomEventPayload: Codable, Hashable { +public protocol CustomEventPayload: Codable, Hashable, Sendable { /// A type all events holding this payload have. static var eventType: EventType { get } } diff --git a/Sources/StreamChat/WebSocketClient/Events/EventDecoder.swift b/Sources/StreamChat/WebSocketClient/Events/EventDecoder.swift index 402e2517f50..2ccc65e4d3d 100644 --- a/Sources/StreamChat/WebSocketClient/Events/EventDecoder.swift +++ b/Sources/StreamChat/WebSocketClient/Events/EventDecoder.swift @@ -22,11 +22,11 @@ struct EventDecoder { } extension ClientError { - public final class IgnoredEventType: ClientError { + public final class IgnoredEventType: ClientError, @unchecked Sendable { override public var localizedDescription: String { "The incoming event type is not supported. Ignoring." } } - public final class EventDecoding: ClientError { + public final class EventDecoding: ClientError, @unchecked Sendable { override init(_ message: String, _ file: StaticString = #file, _ line: UInt = #line) { super.init(message, file, line) } diff --git a/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift b/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift index c760f6358d4..2218531275b 100644 --- a/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift +++ b/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift @@ -7,7 +7,7 @@ import Foundation // MARK: - Temporary /// The DTO object mirroring the JSON representation of an event. -class EventPayload: Decodable { +final class EventPayload: Decodable, Sendable { enum CodingKeys: String, CodingKey, CaseIterable { case eventType = "type" case connectionId = "connection_id" @@ -66,8 +66,8 @@ class EventPayload: Decodable { let lastReadMessageId: MessageId? let lastReadAt: Date? let unreadMessagesCount: Int? - var poll: PollPayload? - var vote: PollVotePayload? + let poll: PollPayload? + let vote: PollVotePayload? /// Thread Data, it is stored in Result, to be easier to debug decoding errors let threadDetails: Result? diff --git a/Sources/StreamChat/WebSocketClient/Events/EventType.swift b/Sources/StreamChat/WebSocketClient/Events/EventType.swift index 1268e2b955e..98a550a686d 100644 --- a/Sources/StreamChat/WebSocketClient/Events/EventType.swift +++ b/Sources/StreamChat/WebSocketClient/Events/EventType.swift @@ -5,7 +5,7 @@ import Foundation /// An event type. -public struct EventType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral { +public struct EventType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable { public let rawValue: String public init(rawValue: String) { @@ -243,13 +243,13 @@ extension EventType { } extension ClientError { - final class UnknownChannelEvent: ClientError { + final class UnknownChannelEvent: ClientError, @unchecked Sendable { init(_ type: EventType) { super.init("Event with \(type) cannot be decoded as system event.") } } - final class UnknownUserEvent: ClientError { + final class UnknownUserEvent: ClientError, @unchecked Sendable { init(_ type: EventType) { super.init("Event with \(type) cannot be decoded as system event.") } diff --git a/Sources/StreamChat/WebSocketClient/Events/MemberEvents.swift b/Sources/StreamChat/WebSocketClient/Events/MemberEvents.swift index efa4b30bffb..a4340b67d56 100644 --- a/Sources/StreamChat/WebSocketClient/Events/MemberEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/MemberEvents.swift @@ -19,7 +19,7 @@ public struct MemberAddedEvent: MemberEvent, ChannelSpecificEvent { public let createdAt: Date } -class MemberAddedEventDTO: EventDTO { +final class MemberAddedEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let member: MemberPayload @@ -64,7 +64,7 @@ public struct MemberUpdatedEvent: MemberEvent, ChannelSpecificEvent { public let createdAt: Date } -class MemberUpdatedEventDTO: EventDTO { +final class MemberUpdatedEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let member: MemberPayload @@ -106,7 +106,7 @@ public struct MemberRemovedEvent: MemberEvent, ChannelSpecificEvent { public let createdAt: Date } -class MemberRemovedEventDTO: EventDTO { +final class MemberRemovedEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let createdAt: Date diff --git a/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift b/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift index 78cb41587a4..d3700c7f9ab 100644 --- a/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift @@ -28,7 +28,7 @@ public struct MessageNewEvent: ChannelSpecificEvent, HasUnreadCount { public let unreadCount: UnreadCount? } -class MessageNewEventDTO: EventDTO { +final class MessageNewEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let message: MessagePayload @@ -84,7 +84,7 @@ public struct MessageUpdatedEvent: ChannelSpecificEvent { public let createdAt: Date } -class MessageUpdatedEventDTO: EventDTO { +final class MessageUpdatedEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let message: MessagePayload @@ -136,7 +136,7 @@ public struct MessageDeletedEvent: ChannelSpecificEvent { public let isHardDelete: Bool } -class MessageDeletedEventDTO: EventDTO { +final class MessageDeletedEventDTO: EventDTO { let user: UserPayload? let cid: ChannelId let message: MessagePayload @@ -199,7 +199,7 @@ public struct MessageReadEvent: ChannelSpecificEvent { public let unreadCount: UnreadCount? } -class MessageReadEventDTO: EventDTO { +final class MessageReadEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let createdAt: Date diff --git a/Sources/StreamChat/WebSocketClient/Events/NotificationEvents.swift b/Sources/StreamChat/WebSocketClient/Events/NotificationEvents.swift index 27d1c7165be..b09661cb73a 100644 --- a/Sources/StreamChat/WebSocketClient/Events/NotificationEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/NotificationEvents.swift @@ -22,7 +22,7 @@ public struct NotificationMessageNewEvent: ChannelSpecificEvent, HasUnreadCount public let unreadCount: UnreadCount? } -class NotificationMessageNewEventDTO: EventDTO { +final class NotificationMessageNewEventDTO: EventDTO { let channel: ChannelDetailPayload let message: MessagePayload let unreadCount: UnreadCountPayload? @@ -65,7 +65,7 @@ public struct NotificationMarkAllReadEvent: Event, HasUnreadCount { public let createdAt: Date } -class NotificationMarkAllReadEventDTO: EventDTO { +final class NotificationMarkAllReadEventDTO: EventDTO { let user: UserPayload let unreadCount: UnreadCountPayload let createdAt: Date @@ -135,7 +135,7 @@ public struct NotificationMarkUnreadEvent: ChannelSpecificEvent { public let unreadMessagesCount: Int } -class NotificationMarkReadEventDTO: EventDTO { +final class NotificationMarkReadEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let unreadCount: UnreadCountPayload @@ -166,7 +166,7 @@ class NotificationMarkReadEventDTO: EventDTO { } } -class NotificationMarkUnreadEventDTO: EventDTO { +final class NotificationMarkUnreadEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let createdAt: Date @@ -215,7 +215,7 @@ public struct NotificationMutesUpdatedEvent: Event { public let createdAt: Date } -class NotificationMutesUpdatedEventDTO: EventDTO { +final class NotificationMutesUpdatedEventDTO: EventDTO { let currentUser: CurrentUserPayload let createdAt: Date let payload: EventPayload @@ -254,7 +254,7 @@ public struct NotificationAddedToChannelEvent: ChannelSpecificEvent, HasUnreadCo public let createdAt: Date } -class NotificationAddedToChannelEventDTO: EventDTO { +final class NotificationAddedToChannelEventDTO: EventDTO { let channel: ChannelDetailPayload let unreadCount: UnreadCountPayload? // This `member` field is equal to the `membership` field in channel query @@ -301,7 +301,7 @@ public struct NotificationRemovedFromChannelEvent: ChannelSpecificEvent { public let createdAt: Date } -class NotificationRemovedFromChannelEventDTO: EventDTO { +final class NotificationRemovedFromChannelEventDTO: EventDTO { let cid: ChannelId let user: UserPayload // This `member` field is equal to the `membership` field in channel query @@ -341,7 +341,7 @@ public struct NotificationChannelMutesUpdatedEvent: Event { public let createdAt: Date } -class NotificationChannelMutesUpdatedEventDTO: EventDTO { +final class NotificationChannelMutesUpdatedEventDTO: EventDTO { let currentUser: CurrentUserPayload let createdAt: Date let payload: EventPayload @@ -377,7 +377,7 @@ public struct NotificationInvitedEvent: MemberEvent, ChannelSpecificEvent { public let createdAt: Date } -class NotificationInvitedEventDTO: EventDTO { +final class NotificationInvitedEventDTO: EventDTO { let user: UserPayload let cid: ChannelId // This `member` field is equal to the `membership` field in channel query @@ -426,7 +426,7 @@ public struct NotificationInviteAcceptedEvent: MemberEvent, ChannelSpecificEvent public let createdAt: Date } -class NotificationInviteAcceptedEventDTO: EventDTO { +final class NotificationInviteAcceptedEventDTO: EventDTO { let user: UserPayload let channel: ChannelDetailPayload // This `member` field is equal to the `membership` field in channel query @@ -476,7 +476,7 @@ public struct NotificationInviteRejectedEvent: MemberEvent, ChannelSpecificEvent public let createdAt: Date } -class NotificationInviteRejectedEventDTO: EventDTO { +final class NotificationInviteRejectedEventDTO: EventDTO { let user: UserPayload let channel: ChannelDetailPayload // This `member` field is equal to the `membership` field in channel query @@ -520,7 +520,7 @@ public struct NotificationChannelDeletedEvent: ChannelSpecificEvent { public let createdAt: Date } -class NotificationChannelDeletedEventDTO: EventDTO { +final class NotificationChannelDeletedEventDTO: EventDTO { let cid: ChannelId let channel: ChannelDetailPayload let createdAt: Date diff --git a/Sources/StreamChat/WebSocketClient/Events/ReactionEvents.swift b/Sources/StreamChat/WebSocketClient/Events/ReactionEvents.swift index 153d096d977..d39615589e9 100644 --- a/Sources/StreamChat/WebSocketClient/Events/ReactionEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/ReactionEvents.swift @@ -22,7 +22,7 @@ public struct ReactionNewEvent: ChannelSpecificEvent { public let createdAt: Date } -class ReactionNewEventDTO: EventDTO { +final class ReactionNewEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let message: MessagePayload @@ -78,7 +78,7 @@ public struct ReactionUpdatedEvent: ChannelSpecificEvent { public let createdAt: Date } -class ReactionUpdatedEventDTO: EventDTO { +final class ReactionUpdatedEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let message: MessagePayload @@ -134,7 +134,7 @@ public struct ReactionDeletedEvent: ChannelSpecificEvent { public let createdAt: Date } -class ReactionDeletedEventDTO: EventDTO { +final class ReactionDeletedEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let message: MessagePayload diff --git a/Sources/StreamChat/WebSocketClient/Events/ThreadEvents.swift b/Sources/StreamChat/WebSocketClient/Events/ThreadEvents.swift index f896b63f901..dd9eb191584 100644 --- a/Sources/StreamChat/WebSocketClient/Events/ThreadEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/ThreadEvents.swift @@ -22,7 +22,7 @@ public struct ThreadMessageNewEvent: Event { public let createdAt: Date } -class ThreadMessageNewEventDTO: EventDTO { +final class ThreadMessageNewEventDTO: EventDTO { let cid: ChannelId let message: MessagePayload let channel: ChannelDetailPayload @@ -64,7 +64,7 @@ public struct ThreadUpdatedEvent: Event { public let createdAt: Date? } -class ThreadUpdatedEventDTO: EventDTO { +final class ThreadUpdatedEventDTO: EventDTO { let thread: ThreadPartialPayload let createdAt: Date let payload: EventPayload diff --git a/Sources/StreamChat/WebSocketClient/Events/TypingEvent.swift b/Sources/StreamChat/WebSocketClient/Events/TypingEvent.swift index 714b96f2d9a..a5dee746234 100644 --- a/Sources/StreamChat/WebSocketClient/Events/TypingEvent.swift +++ b/Sources/StreamChat/WebSocketClient/Events/TypingEvent.swift @@ -25,7 +25,7 @@ public struct TypingEvent: ChannelSpecificEvent { public var isThread: Bool { parentId != nil } } -class TypingEventDTO: EventDTO { +final class TypingEventDTO: EventDTO { let user: UserPayload let cid: ChannelId let isTyping: Bool diff --git a/Sources/StreamChat/WebSocketClient/Events/UserEvents.swift b/Sources/StreamChat/WebSocketClient/Events/UserEvents.swift index 0da70e117b9..af77d2e79b0 100644 --- a/Sources/StreamChat/WebSocketClient/Events/UserEvents.swift +++ b/Sources/StreamChat/WebSocketClient/Events/UserEvents.swift @@ -13,7 +13,7 @@ public struct UserPresenceChangedEvent: Event { public let createdAt: Date? } -class UserPresenceChangedEventDTO: EventDTO { +final class UserPresenceChangedEventDTO: EventDTO { let user: UserPayload let createdAt: Date let payload: EventPayload @@ -43,7 +43,7 @@ public struct UserUpdatedEvent: Event { public let createdAt: Date? } -class UserUpdatedEventDTO: EventDTO { +final class UserUpdatedEventDTO: EventDTO { let user: UserPayload let createdAt: Date let payload: EventPayload @@ -84,7 +84,7 @@ public struct UserWatchingEvent: ChannelSpecificEvent { public let isStarted: Bool } -class UserWatchingEventDTO: EventDTO { +final class UserWatchingEventDTO: EventDTO { let cid: ChannelId let user: UserPayload let createdAt: Date @@ -170,7 +170,7 @@ public struct UserBannedEvent: ChannelSpecificEvent { public let isShadowBan: Bool? } -class UserBannedEventDTO: EventDTO { +final class UserBannedEventDTO: EventDTO { let cid: ChannelId let user: UserPayload let ownerId: UserId @@ -248,7 +248,7 @@ public struct UserUnbannedEvent: ChannelSpecificEvent { public let createdAt: Date? } -class UserUnbannedEventDTO: EventDTO { +final class UserUnbannedEventDTO: EventDTO { let cid: ChannelId let user: UserPayload let createdAt: Date diff --git a/Sources/StreamChat/WebSocketClient/RetryStrategy.swift b/Sources/StreamChat/WebSocketClient/RetryStrategy.swift index 8c9f070de94..3ee95c4ebfa 100644 --- a/Sources/StreamChat/WebSocketClient/RetryStrategy.swift +++ b/Sources/StreamChat/WebSocketClient/RetryStrategy.swift @@ -5,7 +5,7 @@ import Foundation /// The type encapsulating the logic of computing delays for the failed actions that needs to be retried. -protocol RetryStrategy { +protocol RetryStrategy: Sendable { /// Returns the # of consecutively failed retries. var consecutiveFailuresCount: Int { get } diff --git a/Sources/StreamChat/WebSocketClient/WebSocketClient.swift b/Sources/StreamChat/WebSocketClient/WebSocketClient.swift index b48e1410e70..6a3177d729c 100644 --- a/Sources/StreamChat/WebSocketClient/WebSocketClient.swift +++ b/Sources/StreamChat/WebSocketClient/WebSocketClient.swift @@ -4,14 +4,12 @@ import Foundation -class WebSocketClient { +class WebSocketClient: @unchecked Sendable { /// The notification center `WebSocketClient` uses to send notifications about incoming events. let eventNotificationCenter: EventNotificationCenter /// The batch of events received via the web-socket that wait to be processed. - private(set) lazy var eventsBatcher = environment.eventBatcherBuilder { [weak self] events, completion in - self?.eventNotificationCenter.process(events, completion: completion) - } + let eventsBatcher: EventBatcher /// The current state the web socket connection. @Atomic private(set) var connectionState: WebSocketConnectionState = .initialized { @@ -42,13 +40,13 @@ class WebSocketClient { /// /// Changing this value doesn't automatically update the existing connection. You need to manually call `disconnect` /// and `connect` to make a new connection to the updated endpoint. - var connectEndpoint: Endpoint? + @Atomic var connectEndpoint: Endpoint? /// The decoder used to decode incoming events private let eventDecoder: AnyEventDecoder /// The web socket engine used to make the actual WS connection - private(set) var engine: WebSocketEngine? + @Atomic private(set) var engine: WebSocketEngine? /// The queue on which web socket engine methods are called private let engineQueue: DispatchQueue = .init(label: "io.getStream.chat.core.web_socket_engine_queue", qos: .userInitiated) @@ -61,11 +59,7 @@ class WebSocketClient { /// An object containing external dependencies of `WebSocketClient` private let environment: Environment - private(set) lazy var pingController: WebSocketPingController = { - let pingController = environment.createPingController(environment.timerType, engineQueue) - pingController.delegate = self - return pingController - }() + let pingController: WebSocketPingController private func createEngineIfNeeded(for connectEndpoint: Endpoint) throws -> WebSocketEngine { let request: URLRequest @@ -98,6 +92,11 @@ class WebSocketClient { self.eventDecoder = eventDecoder self.eventNotificationCenter = eventNotificationCenter + eventsBatcher = environment.eventBatcherBuilder { [eventNotificationCenter] events, completion in + eventNotificationCenter.process(events, completion: completion) + } + pingController = environment.createPingController(environment.timerType, engineQueue) + pingController.delegate = self } func initialize() { @@ -139,7 +138,7 @@ class WebSocketClient { /// - Parameter source: Additional information about the source of the disconnection. Default value is `.userInitiated`. func disconnect( source: WebSocketConnectionState.DisconnectionSource = .userInitiated, - completion: @escaping () -> Void + completion: @escaping @Sendable() -> Void ) { switch connectionState { case .initialized, .disconnected, .disconnecting: @@ -180,7 +179,7 @@ extension WebSocketClient { } var eventBatcherBuilder: ( - _ handler: @escaping ([Event], @escaping () -> Void) -> Void + _ handler: @escaping ([Event], @escaping @Sendable() -> Void) -> Void ) -> EventBatcher = { Batcher(period: 0.5, handler: $0) } @@ -291,7 +290,7 @@ extension WebSocketClient { #endif extension ClientError { - public final class WebSocket: ClientError {} + public final class WebSocket: ClientError, @unchecked Sendable {} } /// WebSocket Error diff --git a/Sources/StreamChat/WebSocketClient/WebSocketConnectPayload.swift b/Sources/StreamChat/WebSocketClient/WebSocketConnectPayload.swift index 0a782a14440..a3b66a20d9e 100644 --- a/Sources/StreamChat/WebSocketClient/WebSocketConnectPayload.swift +++ b/Sources/StreamChat/WebSocketClient/WebSocketConnectPayload.swift @@ -4,7 +4,7 @@ import Foundation -class WebSocketConnectPayload: Encodable { +final class WebSocketConnectPayload: Encodable, Sendable { enum CodingKeys: String, CodingKey { case userId = "user_id" case userDetails = "user_details" diff --git a/Sources/StreamChat/WebSocketClient/WebSocketPingController.swift b/Sources/StreamChat/WebSocketClient/WebSocketPingController.swift index 31e3772945a..b9eb96082e4 100644 --- a/Sources/StreamChat/WebSocketClient/WebSocketPingController.swift +++ b/Sources/StreamChat/WebSocketClient/WebSocketPingController.swift @@ -15,7 +15,7 @@ protocol WebSocketPingControllerDelegate: AnyObject { /// The controller manages ping and pong timers. It sends ping periodically to keep a web socket connection alive. /// After ping is sent, a pong waiting timer is started, and if pong does not come, a forced disconnect is called. -class WebSocketPingController { +class WebSocketPingController: @unchecked Sendable { /// The time interval to ping connection to keep it alive. static let pingTimeInterval: TimeInterval = 25 /// The time interval for pong timeout. @@ -23,15 +23,21 @@ class WebSocketPingController { private let timerType: Timer.Type private let timerQueue: DispatchQueue + private let queue = DispatchQueue(label: "io.getstream.web-socket-ping-controller", target: .global()) /// The timer used for scheduling `ping` calls - private var pingTimerControl: RepeatingTimerControl? + private var _pingTimerControl: RepeatingTimerControl? /// The pong timeout timer. - private var pongTimeoutTimer: TimerControl? + private var _pongTimeoutTimer: TimerControl? /// A delegate to control `WebSocketClient` connection by `WebSocketPingController`. - weak var delegate: WebSocketPingControllerDelegate? + var delegate: WebSocketPingControllerDelegate? { + get { queue.sync { _delegate } } + set { queue.sync { _delegate = newValue } } + } + + weak var _delegate: WebSocketPingControllerDelegate? deinit { cancelPongTimeoutTimer() @@ -53,11 +59,13 @@ class WebSocketPingController { cancelPongTimeoutTimer() schedulePingTimerIfNeeded() - if connectionState.isConnected { - log.info("Resume WebSocket Ping timer") - pingTimerControl?.resume() - } else { - pingTimerControl?.suspend() + queue.sync { [weak self] in + if connectionState.isConnected { + log.info("Resume WebSocket Ping timer") + self?._pingTimerControl?.resume() + } else { + self?._pingTimerControl?.suspend() + } } } @@ -78,23 +86,30 @@ class WebSocketPingController { // MARK: Timers private func schedulePingTimerIfNeeded() { - guard pingTimerControl == nil else { return } - pingTimerControl = timerType.scheduleRepeating(timeInterval: Self.pingTimeInterval, queue: timerQueue) { [weak self] in - self?.sendPing() + queue.sync { + guard _pingTimerControl == nil else { return } + _pingTimerControl = timerType.scheduleRepeating(timeInterval: Self.pingTimeInterval, queue: self.timerQueue) { [weak self] in + self?.sendPing() + } } } private func schedulePongTimeoutTimer() { cancelPongTimeoutTimer() // Start pong timeout timer. - pongTimeoutTimer = timerType.schedule(timeInterval: Self.pongTimeoutTimeInterval, queue: timerQueue) { [weak self] in - log.info("WebSocket Pong timeout. Reconnect") - self?.delegate?.disconnectOnNoPongReceived() + queue.sync { + self._pongTimeoutTimer = self.timerType.schedule(timeInterval: Self.pongTimeoutTimeInterval, queue: self.timerQueue) { [weak self] in + log.info("WebSocket Pong timeout. Reconnect") + self?.delegate?.disconnectOnNoPongReceived() + } } } private func cancelPongTimeoutTimer() { - pongTimeoutTimer?.cancel() - pongTimeoutTimer = nil + // Called from deinit, must be sync + queue.sync { + _pongTimeoutTimer?.cancel() + _pongTimeoutTimer = nil + } } } diff --git a/Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift b/Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift index c05791ec9d8..4b423d66401 100644 --- a/Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift +++ b/Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift @@ -19,17 +19,17 @@ import Foundation /// - Upload attachments in order declared by `locallyCreatedAt` /// - Start uploading attachments when connection status changes (offline -> online) /// -class AttachmentQueueUploader: Worker { +class AttachmentQueueUploader: Worker, @unchecked Sendable { @Atomic private var pendingAttachmentIDs: Set = [] private let observer: StateLayerDatabaseObserver private let attachmentPostProcessor: UploadedAttachmentPostProcessor? private let attachmentUpdater = AnyAttachmentUpdater() private let attachmentStorage = AttachmentStorage() - private var continuations = [AttachmentId: CheckedContinuation]() private let queue = DispatchQueue(label: "co.getStream.ChatClient.AttachmentQueueUploader", target: .global(qos: .utility)) - private var fileUploadConfig: AppSettings.UploadConfig? - private var imageUploadConfig: AppSettings.UploadConfig? + private var _continuations = [AttachmentId: CheckedContinuation]() + private var _fileUploadConfig: AppSettings.UploadConfig? + private var _imageUploadConfig: AppSettings.UploadConfig? var minSignificantUploadingProgressChange: Double = 0.05 @@ -48,8 +48,8 @@ class AttachmentQueueUploader: Worker { func setAppSettings(_ appSettings: AppSettings?) { queue.async { [weak self] in - self?.fileUploadConfig = appSettings?.fileUploadConfig - self?.imageUploadConfig = appSettings?.imageUploadConfig + self?._fileUploadConfig = appSettings?.fileUploadConfig + self?._imageUploadConfig = appSettings?.imageUploadConfig } } @@ -95,7 +95,7 @@ class AttachmentQueueUploader: Worker { attachmentId: id, uploadedAttachment: nil, newState: .uploadingFailed, - completion: { + completion: { [weak self] in self?.removePendingAttachment(with: id, result: .failure(error)) } ) @@ -103,7 +103,7 @@ class AttachmentQueueUploader: Worker { case .success(let attachment): self?.apiClient.uploadAttachment( attachment, - progress: { + progress: { [weak self] in self?.updateAttachmentIfNeeded( attachmentId: id, uploadedAttachment: nil, @@ -111,12 +111,12 @@ class AttachmentQueueUploader: Worker { completion: {} ) }, - completion: { result in + completion: { [weak self] result in self?.updateAttachmentIfNeeded( attachmentId: id, uploadedAttachment: result.value, newState: result.error == nil ? .uploaded : .uploadingFailed, - completion: { + completion: { [weak self] in self?.removePendingAttachment(with: id, result: result) } ) @@ -126,11 +126,9 @@ class AttachmentQueueUploader: Worker { } } - private func prepareAttachmentForUpload(with id: AttachmentId, completion: @escaping (Result) -> Void) { + private func prepareAttachmentForUpload(with id: AttachmentId, completion: @escaping @Sendable(Result) -> Void) { let attachmentStorage = self.attachmentStorage - var model: AnyChatMessageAttachment? - var attachmentLocalURL: URL? - database.write { session in + database.write(converting: { session in guard let attachment = session.attachment(id: id) else { throw ClientError.AttachmentDoesNotExist(id: id) } @@ -143,40 +141,42 @@ class AttachmentQueueUploader: Worker { log.error("Could not copy attachment to local storage: \(error.localizedDescription)", subsystems: .offlineSupport) } } - attachmentLocalURL = attachment.localURL - model = attachment.asAnyModel() - } completion: { [weak self] error in - DispatchQueue.main.async { - if let model { + guard let model = attachment.asAnyModel() else { + throw ClientError.AttachmentDoesNotExist(id: id) + } + return (localURL: attachment.localURL, model: model) + }, completion: { [weak self] result in + DispatchQueue.main.async { [weak self] in + switch result { + case .success(let writeData): // Attachment uploading state should be validated after preparing the local file (for ensuring the local file persists for retry) - if let attachmentLocalURL, self?.isAllowedToUpload(model.type, localURL: attachmentLocalURL) == false { + if let attachmentLocalURL = writeData.localURL, + self?.isAllowedToUpload(writeData.model.type, localURL: attachmentLocalURL) == false { completion( .failure( ClientError.AttachmentUploadBlocked( id: id, - attachmentType: model.type, + attachmentType: writeData.model.type, pathExtension: attachmentLocalURL.pathExtension ) ) ) } else { - completion(.success(model)) + completion(.success(writeData.model)) } - } else if let error { + case .failure(let error): completion(.failure(error)) - } else { - completion(.failure(ClientError.Unknown("Incorrect completion handling in AttachmentQueueUploader"))) } } - } + }) } private func isAllowedToUpload(_ attachmentType: AttachmentType, localURL: URL) -> Bool { switch attachmentType { case .image: - return queue.sync { imageUploadConfig?.isAllowed(localURL: localURL) ?? true } + return queue.sync { _imageUploadConfig?.isAllowed(localURL: localURL) ?? true } case .audio, .file, .unknown, .video, .voiceRecording: - return queue.sync { fileUploadConfig?.isAllowed(localURL: localURL) ?? true } + return queue.sync { _fileUploadConfig?.isAllowed(localURL: localURL) ?? true } default: return true } @@ -191,7 +191,7 @@ class AttachmentQueueUploader: Worker { attachmentId: AttachmentId, uploadedAttachment: UploadedAttachment?, newState: LocalAttachmentState, - completion: @escaping () -> Void = {} + completion: @escaping @Sendable() -> Void = {} ) { database.write({ [minSignificantUploadingProgressChange, weak self] session in guard let attachmentDTO = session.attachment(id: attachmentId) else { return } @@ -295,21 +295,19 @@ private extension Array where Element == ListChange { } } -private class AttachmentStorage { +private final class AttachmentStorage: Sendable { enum Constants { static let path = "LocalAttachments" } - private let fileManager: FileManager - private lazy var baseURL: URL = { - let base = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first ?? fileManager.temporaryDirectory + private let baseURL: URL = { + let base = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory return base.appendingPathComponent(Constants.path) }() - init(fileManager: FileManager = .default) { - self.fileManager = fileManager + init() { do { - try fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) } catch { log.error("Could not create a directory to store attachments: \(error.localizedDescription)") } @@ -338,14 +336,14 @@ private class AttachmentStorage { func removeAttachment(at localURL: URL) { guard fileExists(at: localURL) else { return } do { - try fileManager.removeItem(at: localURL) + try FileManager.default.removeItem(at: localURL) } catch { log.info("Unable to remove attachment at \(localURL): \(error.localizedDescription)") } } private func fileExists(at url: URL) -> Bool { - fileManager.fileExists(atPath: url.path) + FileManager.default.fileExists(atPath: url.path) } } @@ -363,7 +361,7 @@ extension AttachmentQueueUploader { continuation: CheckedContinuation ) { queue.async { - self.continuations[attachmentId] = continuation + self._continuations[attachmentId] = continuation } } @@ -372,7 +370,7 @@ extension AttachmentQueueUploader { result: Result ) { queue.async { - guard let continuation = self.continuations.removeValue(forKey: attachmentId) else { return } + guard let continuation = self._continuations.removeValue(forKey: attachmentId) else { return } continuation.resume(with: result) } } diff --git a/Sources/StreamChat/Workers/Background/ConnectionRecoveryHandler.swift b/Sources/StreamChat/Workers/Background/ConnectionRecoveryHandler.swift index cf4bcba6d68..6a6e9e3ede9 100644 --- a/Sources/StreamChat/Workers/Background/ConnectionRecoveryHandler.swift +++ b/Sources/StreamChat/Workers/Background/ConnectionRecoveryHandler.swift @@ -23,7 +23,7 @@ protocol ConnectionRecoveryHandler: ConnectionStateDelegate { /// We remember `lastReceivedEventDate` when state becomes `connecting` to catch the last event date /// before the `HealthCheck` override the `lastReceivedEventDate` with the recent date. /// -final class DefaultConnectionRecoveryHandler: ConnectionRecoveryHandler { +final class DefaultConnectionRecoveryHandler: ConnectionRecoveryHandler, Sendable { // MARK: - Properties private let webSocketClient: WebSocketClient @@ -32,9 +32,10 @@ final class DefaultConnectionRecoveryHandler: ConnectionRecoveryHandler { private let backgroundTaskScheduler: BackgroundTaskScheduler? private let internetConnection: InternetConnection private let reconnectionTimerType: Timer.Type - private var reconnectionStrategy: RetryStrategy - private var reconnectionTimer: TimerControl? private let keepConnectionAliveInBackground: Bool + private let queue = DispatchQueue(label: "io.getstream.default-connection-recovery-handler", target: .global()) + nonisolated(unsafe) private var reconnectionStrategy: RetryStrategy + nonisolated(unsafe) private var reconnectionTimer: TimerControl? // MARK: - Init @@ -167,7 +168,9 @@ extension DefaultConnectionRecoveryHandler { cancelReconnectionTimer() case .connected: - reconnectionStrategy.resetConsecutiveFailures() + queue.sync { + self.reconnectionStrategy.resetConsecutiveFailures() + } syncRepository.syncLocalState { log.info("Local state sync completed", subsystems: .offlineSupport) } @@ -235,30 +238,35 @@ private extension DefaultConnectionRecoveryHandler { } func scheduleReconnectionTimer() { - let delay = reconnectionStrategy.getDelayAfterTheFailure() + let delay = queue.sync { reconnectionStrategy.getDelayAfterTheFailure() } log.debug("Timer ⏳ \(delay) sec", subsystems: .webSocket) - reconnectionTimer = reconnectionTimerType.schedule( - timeInterval: delay, - queue: .main, - onFire: { [weak self] in - log.debug("Timer 🔥", subsystems: .webSocket) - - if self?.canReconnectAutomatically == true { - self?.webSocketClient.connect() + queue.sync { + reconnectionTimer = reconnectionTimerType.schedule( + timeInterval: delay, + queue: .main, + onFire: { [weak self] in + log.debug("Timer 🔥", subsystems: .webSocket) + + if self?.canReconnectAutomatically == true { + self?.webSocketClient.connect() + } } - } - ) + ) + } } func cancelReconnectionTimer() { - guard reconnectionTimer != nil else { return } - - log.debug("Timer ❌", subsystems: .webSocket) - - reconnectionTimer?.cancel() - reconnectionTimer = nil + // Must be sync, called from deinit + queue.sync { + guard reconnectionTimer != nil else { return } + + log.debug("Timer ❌", subsystems: .webSocket) + + reconnectionTimer?.cancel() + reconnectionTimer = nil + } } var canReconnectAutomatically: Bool { diff --git a/Sources/StreamChat/Workers/Background/MessageEditor.swift b/Sources/StreamChat/Workers/Background/MessageEditor.swift index e3c4ccecea8..270fd0b59aa 100644 --- a/Sources/StreamChat/Workers/Background/MessageEditor.swift +++ b/Sources/StreamChat/Workers/Background/MessageEditor.swift @@ -18,12 +18,12 @@ import Foundation /// - Message edit retry /// - Start editing messages when connection status changes (offline -> online) /// -class MessageEditor: Worker { +class MessageEditor: Worker, @unchecked Sendable { @Atomic private var pendingMessageIDs: Set = [] private let observer: StateLayerDatabaseObserver private let messageRepository: MessageRepository - private var continuations = [MessageId: CheckedContinuation]() + private var _continuations = [MessageId: CheckedContinuation]() private let continuationsQueue = DispatchQueue(label: "co.getStream.ChatClient.MessageEditor") init(messageRepository: MessageRepository, database: DatabaseContainer, apiClient: APIClient) { @@ -78,14 +78,15 @@ class MessageEditor: Worker { } let requestBody = dto.asRequestBody() as MessageRequestBody - messageRepository?.updateMessage(withID: messageId, localState: .syncing) { _ in - self?.apiClient.request(endpoint: .editMessage(payload: requestBody, skipEnrichUrl: dto.skipEnrichUrl)) { apiResult in + let skipEnrichUrl = dto.skipEnrichUrl + messageRepository?.updateMessage(withID: messageId, localState: .syncing) { [weak self, weak messageRepository] _ in + self?.apiClient.request(endpoint: .editMessage(payload: requestBody, skipEnrichUrl: skipEnrichUrl)) { [weak self, weak messageRepository] apiResult in let newMessageState: LocalMessageState? = apiResult.error == nil ? nil : .syncingFailed messageRepository?.updateMessage( withID: messageId, localState: newMessageState - ) { updateResult in + ) { [weak self] updateResult in switch apiResult { case .success: self?.removeMessageIDAndContinue(messageId, result: updateResult) @@ -132,7 +133,7 @@ extension MessageEditor { continuation: CheckedContinuation ) { continuationsQueue.async { - self.continuations[messageId] = continuation + self._continuations[messageId] = continuation } } @@ -141,7 +142,7 @@ extension MessageEditor { result: Result ) { continuationsQueue.async { - guard let continuation = self.continuations.removeValue(forKey: messageId) else { return } + guard let continuation = self._continuations.removeValue(forKey: messageId) else { return } continuation.resume(with: result) } } diff --git a/Sources/StreamChat/Workers/Background/MessageSender.swift b/Sources/StreamChat/Workers/Background/MessageSender.swift index f12a1290828..49b387c5c30 100644 --- a/Sources/StreamChat/Workers/Background/MessageSender.swift +++ b/Sources/StreamChat/Workers/Background/MessageSender.swift @@ -16,17 +16,12 @@ import Foundation /// state of is changed to `sendingFailed`. /// 5. When connection errors happen, all the queued messages are sent to offline retry which retries them one by one. /// -class MessageSender: Worker { +class MessageSender: Worker, @unchecked Sendable { /// Because we need to be sure messages for every channel are sent in the correct order, we create a sending queue for /// every cid. These queues can send messages in parallel. @Atomic private var sendingQueueByCid: [ChannelId: MessageSendingQueue] = [:] private var continuations = [MessageId: CheckedContinuation]() - - private lazy var observer = StateLayerDatabaseObserver( - context: self.database.backgroundReadOnlyContext, - fetchRequest: MessageDTO - .messagesPendingSendFetchRequest() - ) + private let observer: StateLayerDatabaseObserver private let sendingDispatchQueue: DispatchQueue = .init( label: "co.getStream.ChatClient.MessageSenderQueue", @@ -45,9 +40,12 @@ class MessageSender: Worker { ) { self.messageRepository = messageRepository self.eventsNotificationCenter = eventsNotificationCenter + observer = StateLayerDatabaseObserver( + context: database.backgroundReadOnlyContext, + fetchRequest: MessageDTO + .messagesPendingSendFetchRequest() + ) super.init(database: database, apiClient: apiClient) - // We need to initialize the observer synchronously - _ = observer // The rest can be done on a background queue sendingDispatchQueue.async { [weak self] in @@ -158,12 +156,17 @@ private protocol MessageSendingQueueDelegate: AnyObject { } /// This objects takes care of sending messages to the server in the order they have been enqueued. -private class MessageSendingQueue { +private class MessageSendingQueue: @unchecked Sendable { let messageRepository: MessageRepository let eventsNotificationCenter: EventNotificationCenter - let dispatchQueue: DispatchQueue - weak var delegate: MessageSendingQueueDelegate? - + let sendingQueue: DispatchQueue + private let queue = DispatchQueue(label: "io.getstream.message-sending-queue", target: .global()) + private weak var _delegate: MessageSendingQueueDelegate? + weak var delegate: MessageSendingQueueDelegate? { + get { queue.sync { _delegate } } + set { queue.sync { _delegate = newValue } } + } + init( messageRepository: MessageRepository, eventsNotificationCenter: EventNotificationCenter, @@ -171,7 +174,7 @@ private class MessageSendingQueue { ) { self.messageRepository = messageRepository self.eventsNotificationCenter = eventsNotificationCenter - self.dispatchQueue = dispatchQueue + sendingQueue = dispatchQueue } /// We use Set because the message Id is the main identifier. Thanks to this, it's possible to schedule message for sending @@ -206,7 +209,7 @@ private class MessageSendingQueue { /// Gets the oldest message from the queue and tries to send it. private func sendNextMessage() { - dispatchQueue.async { [weak self] in + sendingQueue.async { [weak self] in guard let self else { return } guard let request = self.sortedQueuedRequests.first else { return } diff --git a/Sources/StreamChat/Workers/ChannelListLinker.swift b/Sources/StreamChat/Workers/ChannelListLinker.swift index 14ed489edc3..8d1dc281b53 100644 --- a/Sources/StreamChat/Workers/ChannelListLinker.swift +++ b/Sources/StreamChat/Workers/ChannelListLinker.swift @@ -11,17 +11,17 @@ import Foundation /// so we also analyse if it should be added to the current query. /// - Channel is updated: We only check if we should remove it from the current query. /// We don't try to add it to the current query to not mess with pagination. -final class ChannelListLinker { +final class ChannelListLinker: Sendable { private let clientConfig: ChatClientConfig private let databaseContainer: DatabaseContainer - private var eventObservers = [EventObserver]() - private let filter: ((ChatChannel) -> Bool)? + nonisolated(unsafe) private var eventObservers = [EventObserver]() + private let filter: (@Sendable(ChatChannel) -> Bool)? private let query: ChannelListQuery private let worker: ChannelListUpdater init( query: ChannelListQuery, - filter: ((ChatChannel) -> Bool)?, + filter: (@Sendable(ChatChannel) -> Bool)?, clientConfig: ChatClientConfig, databaseContainer: DatabaseContainer, worker: ChannelListUpdater diff --git a/Sources/StreamChat/Workers/ChannelListUpdater.swift b/Sources/StreamChat/Workers/ChannelListUpdater.swift index 5f8de35b898..942161ddafa 100644 --- a/Sources/StreamChat/Workers/ChannelListUpdater.swift +++ b/Sources/StreamChat/Workers/ChannelListUpdater.swift @@ -5,7 +5,7 @@ import CoreData /// Makes a channels query call to the backend and updates the local storage with the results. -class ChannelListUpdater: Worker { +class ChannelListUpdater: Worker, @unchecked Sendable { /// Makes a channels query call to the backend and updates the local storage with the results. /// /// - Parameters: @@ -14,13 +14,13 @@ class ChannelListUpdater: Worker { /// func update( channelListQuery: ChannelListQuery, - completion: ((Result<[ChatChannel], Error>) -> Void)? = nil + completion: (@Sendable(Result<[ChatChannel], Error>) -> Void)? = nil ) { fetch(channelListQuery: channelListQuery) { [weak self] in switch $0 { case let .success(channelListPayload): let isInitialFetch = channelListQuery.pagination.cursor == nil && channelListQuery.pagination.offset == 0 - var initialActions: ((DatabaseSession) -> Void)? + var initialActions: (@Sendable(DatabaseSession) -> Void)? if isInitialFetch { initialActions = { session in let filterHash = channelListQuery.filter.filterHash @@ -41,7 +41,7 @@ class ChannelListUpdater: Worker { } } - func refreshLoadedChannels(for query: ChannelListQuery, channelCount: Int, completion: @escaping (Result, Error>) -> Void) { + func refreshLoadedChannels(for query: ChannelListQuery, channelCount: Int, completion: @escaping @Sendable(Result, Error>) -> Void) { guard channelCount > 0 else { completion(.success(Set())) return @@ -65,7 +65,7 @@ class ChannelListUpdater: Worker { } } - private func refreshLoadedChannels(for pageQueries: [ChannelListQuery], refreshedChannelIds: Set, completion: @escaping (Result, Error>) -> Void) { + private func refreshLoadedChannels(for pageQueries: [ChannelListQuery], refreshedChannelIds: Set, completion: @escaping @Sendable(Result, Error>) -> Void) { guard let nextQuery = pageQueries.first else { completion(.success(refreshedChannelIds)) return @@ -78,7 +78,7 @@ class ChannelListUpdater: Worker { self?.writeChannelListPayload( payload: channelListPayload, query: nextQuery, - completion: { writeResult in + completion: { [weak self] writeResult in switch writeResult { case .success(let writtenChannels): self?.refreshLoadedChannels( @@ -102,7 +102,7 @@ class ChannelListUpdater: Worker { /// - Parameters: /// - ids: The channel ids. /// - completion: The callback once the request is complete. - func startWatchingChannels(withIds ids: [ChannelId], completion: ((Error?) -> Void)? = nil) { + func startWatchingChannels(withIds ids: [ChannelId], completion: (@Sendable(Error?) -> Void)? = nil) { var query = ChannelListQuery(filter: .in(.cid, values: ids)) query.options = .all @@ -110,7 +110,7 @@ class ChannelListUpdater: Worker { switch $0 { case let .success(payload): self?.database.write { session in - session.saveChannelList(payload: payload, query: nil) + _ = session.saveChannelList(payload: payload, query: nil) } completion: { _ in completion?(nil) } @@ -127,7 +127,7 @@ class ChannelListUpdater: Worker { /// - completion: The completion to call with the results. func fetch( channelListQuery: ChannelListQuery, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { apiClient.request( endpoint: .channels(query: channelListQuery), @@ -137,14 +137,14 @@ class ChannelListUpdater: Worker { /// Marks all channels for a user as read. /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func markAllRead(completion: ((Error?) -> Void)? = nil) { + func markAllRead(completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .markAllRead()) { completion?($0.error) } } /// Links a channel to the given query. - func link(channel: ChatChannel, with query: ChannelListQuery, completion: ((Error?) -> Void)? = nil) { + func link(channel: ChatChannel, with query: ChannelListQuery, completion: (@Sendable(Error?) -> Void)? = nil) { database.write { session in guard let (channelDTO, queryDTO) = session.getChannelWithQuery(cid: channel.cid, query: query) else { return @@ -156,7 +156,7 @@ class ChannelListUpdater: Worker { } /// Unlinks a channel to the given query. - func unlink(channel: ChatChannel, with query: ChannelListQuery, completion: ((Error?) -> Void)? = nil) { + func unlink(channel: ChatChannel, with query: ChannelListQuery, completion: (@Sendable(Error?) -> Void)? = nil) { database.write { session in guard let (channelDTO, queryDTO) = session.getChannelWithQuery(cid: channel.cid, query: query) else { return @@ -188,21 +188,15 @@ private extension ChannelListUpdater { func writeChannelListPayload( payload: ChannelListPayload, query: ChannelListQuery, - initialActions: ((DatabaseSession) -> Void)? = nil, - completion: ((Result<[ChatChannel], Error>) -> Void)? = nil + initialActions: (@Sendable(DatabaseSession) -> Void)? = nil, + completion: (@Sendable(Result<[ChatChannel], Error>) -> Void)? = nil ) { - var channels: [ChatChannel] = [] - database.write { session in + database.write(converting: { session in initialActions?(session) - channels = session.saveChannelList(payload: payload, query: query).compactMap { try? $0.asModel() } - } completion: { error in - if let error = error { - log.error("Failed to save `ChannelListPayload` to the database. Error: \(error)") - completion?(.failure(error)) - } else { - completion?(.success(channels)) - } - } + return session.saveChannelList(payload: payload, query: query).compactMap { try? $0.asModel() } + }, completion: { + completion?($0) + }) } } diff --git a/Sources/StreamChat/Workers/ChannelMemberListUpdater.swift b/Sources/StreamChat/Workers/ChannelMemberListUpdater.swift index fe8046f36bc..2fb6f810c77 100644 --- a/Sources/StreamChat/Workers/ChannelMemberListUpdater.swift +++ b/Sources/StreamChat/Workers/ChannelMemberListUpdater.swift @@ -5,12 +5,12 @@ import CoreData /// Makes a channel members query call to the backend and updates the local storage with the results. -class ChannelMemberListUpdater: Worker { +class ChannelMemberListUpdater: Worker, @unchecked Sendable { /// Makes a channel members query call to the backend and updates the local storage with the results. /// - Parameters: /// - query: The query used in the request. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func load(_ query: ChannelMemberListQuery, completion: ((Result<[ChatChannelMember], Error>) -> Void)? = nil) { + func load(_ query: ChannelMemberListQuery, completion: (@Sendable(Result<[ChatChannelMember], Error>) -> Void)? = nil) { fetchAndSaveChannelIfNeeded(query.cid) { [weak self] error in if let error { completion?(.failure(error)) @@ -18,24 +18,18 @@ class ChannelMemberListUpdater: Worker { } let membersEndpoint: Endpoint = .channelMembers(query: query) - self?.apiClient.request(endpoint: membersEndpoint) { membersResult in + self?.apiClient.request(endpoint: membersEndpoint) { [weak self] membersResult in switch membersResult { case let .success(memberListPayload): - var members = [ChatChannelMember]() - self?.database.write({ session in - members = try session.saveMembers( + self?.database.write(converting: { session in + try session.saveMembers( payload: memberListPayload, channelId: query.cid, query: query ) .map { try $0.asModel() } - }, completion: { error in - if let error = error { - log.error("Failed to save `ChannelMemberListQuery` related data to the database. Error: \(error)") - completion?(.failure(error)) - } else { - completion?(.success(members)) - } + }, completion: { + completion?($0) }) case let .failure(error): completion?(.failure(error)) @@ -64,13 +58,13 @@ extension ChannelMemberListUpdater { // MARK: - Private private extension ChannelMemberListUpdater { - func fetchAndSaveChannelIfNeeded(_ cid: ChannelId, completion: @escaping (Error?) -> Void) { + func fetchAndSaveChannelIfNeeded(_ cid: ChannelId, completion: @escaping @Sendable(Error?) -> Void) { checkChannelExistsLocally(with: cid) { [weak self] exists in exists ? completion(nil) : self?.fetchAndSaveChannel(with: cid, completion: completion) } } - func fetchAndSaveChannel(with cid: ChannelId, completion: @escaping (Error?) -> Void) { + func fetchAndSaveChannel(with cid: ChannelId, completion: @escaping @Sendable(Error?) -> Void) { let query = ChannelQuery(cid: cid) apiClient.request(endpoint: .updateChannel(query: query)) { [weak self] in switch $0 { diff --git a/Sources/StreamChat/Workers/ChannelMemberUpdater.swift b/Sources/StreamChat/Workers/ChannelMemberUpdater.swift index 9674e51f4b1..118d244e1a7 100644 --- a/Sources/StreamChat/Workers/ChannelMemberUpdater.swift +++ b/Sources/StreamChat/Workers/ChannelMemberUpdater.swift @@ -5,7 +5,7 @@ import Foundation /// Makes channel member related calls to the backend. -class ChannelMemberUpdater: Worker { +class ChannelMemberUpdater: Worker, @unchecked Sendable { /// Updates the channel member with additional information. /// - Parameters: /// - userId: The user id of the member. @@ -17,7 +17,7 @@ class ChannelMemberUpdater: Worker { in cid: ChannelId, updates: MemberUpdatePayload?, unset: [String]?, - completion: @escaping ((Result) -> Void) + completion: @escaping (@Sendable(Result) -> Void) ) { apiClient.request( endpoint: .partialMemberUpdate( @@ -43,7 +43,7 @@ class ChannelMemberUpdater: Worker { _ isPinned: Bool, userId: UserId, cid: ChannelId, - completion: @escaping (Error?) -> Void + completion: @escaping @Sendable(Error?) -> Void ) { partialUpdate( userId: userId, @@ -70,7 +70,7 @@ class ChannelMemberUpdater: Worker { _ isArchived: Bool, userId: UserId, cid: ChannelId, - completion: @escaping (Error?) -> Void + completion: @escaping @Sendable(Error?) -> Void ) { partialUpdate( userId: userId, @@ -107,7 +107,7 @@ class ChannelMemberUpdater: Worker { shadow: Bool, for timeoutInMinutes: Int? = nil, reason: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request( endpoint: .banMember(userId, cid: cid, shadow: shadow, timeoutInMinutes: timeoutInMinutes, reason: reason) @@ -124,7 +124,7 @@ class ChannelMemberUpdater: Worker { func unbanMember( _ userId: UserId, in cid: ChannelId, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .unbanMember(userId, cid: cid)) { completion?($0.error) diff --git a/Sources/StreamChat/Workers/ChannelUpdater.swift b/Sources/StreamChat/Workers/ChannelUpdater.swift index 78c4eb22617..bbd59fe69ef 100644 --- a/Sources/StreamChat/Workers/ChannelUpdater.swift +++ b/Sources/StreamChat/Workers/ChannelUpdater.swift @@ -5,7 +5,7 @@ import Foundation /// Makes a channel query call to the backend and updates the local storage with the results. -class ChannelUpdater: Worker { +class ChannelUpdater: Worker, @unchecked Sendable { private let channelRepository: ChannelRepository private let messageRepository: MessageRepository let paginationStateHandler: MessagesPaginationStateHandling @@ -43,9 +43,9 @@ class ChannelUpdater: Worker { func update( channelQuery: ChannelQuery, isInRecoveryMode: Bool, - onChannelCreated: ((ChannelId) -> Void)? = nil, + onChannelCreated: (@Sendable(ChannelId) -> Void)? = nil, actions: ChannelUpdateActions? = nil, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { if let pagination = channelQuery.pagination { paginationStateHandler.begin(pagination: pagination) @@ -58,7 +58,7 @@ class ChannelUpdater: Worker { let resetWatchers = didLoadFirstPage let isChannelCreate = onChannelCreated != nil - let completion: (Result) -> Void = { [weak database] result in + let completion: @Sendable(Result) -> Void = { [weak database] result in do { if let pagination = channelQuery.pagination { self.paginationStateHandler.end(pagination: pagination, with: result.map(\.messages)) @@ -128,7 +128,7 @@ class ChannelUpdater: Worker { /// - Parameters: /// - channelPayload: New channel data. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func updateChannel(channelPayload: ChannelEditDetailPayload, completion: ((Error?) -> Void)? = nil) { + func updateChannel(channelPayload: ChannelEditDetailPayload, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .updateChannel(channelPayload: channelPayload)) { completion?($0.error) } @@ -142,7 +142,7 @@ class ChannelUpdater: Worker { func partialChannelUpdate( updates: ChannelEditDetailPayload, unsetProperties: [String], - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .partialChannelUpdate(updates: updates, unsetProperties: unsetProperties)) { completion?($0.error) @@ -156,7 +156,7 @@ class ChannelUpdater: Worker { in cid: ChannelId, membersPagination: Pagination, memberListSorting: [Sorting], - completion: @escaping (Result<([ChatChannelMember]), Error>) -> Void + completion: @escaping @Sendable(Result<([ChatChannelMember]), Error>) -> Void ) { if membersPagination.pageSize <= 0 { completion(.success([])) @@ -172,10 +172,9 @@ class ChannelUpdater: Worker { ) channelQuery.options = .state apiClient.request(endpoint: .updateChannel(query: channelQuery)) { [database] result in - var paginatedMembers: [ChatChannelMember]? switch result { case .success(let payload): - database.write { session in + database.write(converting: { session in // State layer uses member list query to return all the paginated members // In addition to this, we want to save channel data because reads are // stored and returned through channel data. @@ -192,14 +191,8 @@ class ChannelUpdater: Worker { let memberListQueryDTO = try session.saveQuery(memberListQuery) memberListQueryDTO.members.formUnion(updatedChannel.members) - paginatedMembers = payload.members.compactMapLoggingError { try session.member(userId: $0.userId, cid: cid)?.asModel() } - } completion: { error in - if let paginatedMembers { - completion(.success(paginatedMembers)) - } else { - completion(.failure(error ?? ClientError.Unknown())) - } - } + return payload.members.compactMapLoggingError { try session.member(userId: $0.userId, cid: cid)?.asModel() } + }, completion: completion) case .failure(let apiError): completion(.failure(apiError)) } @@ -212,7 +205,7 @@ class ChannelUpdater: Worker { /// - mute: Defines if the channel with the specified **cid** should be muted. /// - expiration: Duration of mute in milliseconds. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func muteChannel(cid: ChannelId, mute: Bool, expiration: Int? = nil, completion: ((Error?) -> Void)? = nil) { + func muteChannel(cid: ChannelId, mute: Bool, expiration: Int? = nil, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .muteChannel(cid: cid, mute: mute, expiration: expiration)) { completion?($0.error) } @@ -222,7 +215,7 @@ class ChannelUpdater: Worker { /// - Parameters: /// - cid: The channel identifier. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func deleteChannel(cid: ChannelId, completion: ((Error?) -> Void)? = nil) { + func deleteChannel(cid: ChannelId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .deleteChannel(cid: cid)) { [weak self] result in switch result { case .success: @@ -253,7 +246,7 @@ class ChannelUpdater: Worker { skipPush: Bool = false, hardDelete: Bool = true, systemMessage: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { guard let message = systemMessage else { truncate(cid: cid, skipPush: skipPush, hardDelete: hardDelete, completion: completion) @@ -298,7 +291,7 @@ class ChannelUpdater: Worker { skipPush: Bool = false, hardDelete: Bool = true, requestBody: MessageRequestBody? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .truncateChannel(cid: cid, skipPush: skipPush, hardDelete: hardDelete, message: requestBody)) { if let error = $0.error { @@ -313,7 +306,7 @@ class ChannelUpdater: Worker { /// - cid: The channel identifier. /// - clearHistory: Flag to remove channel history. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func hideChannel(cid: ChannelId, clearHistory: Bool, completion: ((Error?) -> Void)? = nil) { + func hideChannel(cid: ChannelId, clearHistory: Bool, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .hideChannel(cid: cid, clearHistory: clearHistory)) { [weak self] result in if result.error == nil { // If the API call is a success, we mark the channel as hidden @@ -340,7 +333,7 @@ class ChannelUpdater: Worker { /// - Parameters: /// - channel: The channel you want to show. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func showChannel(cid: ChannelId, completion: ((Error?) -> Void)? = nil) { + func showChannel(cid: ChannelId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .showChannel(cid: cid)) { completion?($0.error) } @@ -379,10 +372,9 @@ class ChannelUpdater: Worker { restrictedVisibility: [UserId] = [], poll: PollPayload? = nil, extraData: [String: RawJSON], - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { - var newMessage: ChatMessage? - database.write({ (session) in + database.write(converting: { (session) in let newMessageDTO = try session.createNewMessage( in: cid, messageId: messageId, @@ -408,14 +400,10 @@ class ChannelUpdater: Worker { newMessageDTO.showInsideThread = true } newMessageDTO.localMessageState = .pendingSend - newMessage = try newMessageDTO.asModel() - }) { error in - if let message = newMessage, error == nil { - completion?(.success(message)) - } else { - completion?(.failure(error ?? ClientError.Unknown())) - } - } + return try newMessageDTO.asModel() + }, completion: { + completion?($0) + }) } /// Add users to the channel as members. @@ -432,7 +420,7 @@ class ChannelUpdater: Worker { members: [MemberInfo], message: String? = nil, hideHistory: Bool, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { let messagePayload = messagePayload(text: message, currentUserId: currentUserId) apiClient.request( @@ -459,7 +447,7 @@ class ChannelUpdater: Worker { cid: ChannelId, userIds: Set, message: String? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { let messagePayload = messagePayload(text: message, currentUserId: currentUserId) apiClient.request( @@ -481,7 +469,7 @@ class ChannelUpdater: Worker { func inviteMembers( cid: ChannelId, userIds: Set, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .inviteMembers(cid: cid, userIds: userIds)) { completion?($0.error) @@ -496,7 +484,7 @@ class ChannelUpdater: Worker { func acceptInvite( cid: ChannelId, message: String?, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .acceptInvite(cid: cid, message: message)) { completion?($0.error) @@ -509,7 +497,7 @@ class ChannelUpdater: Worker { /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. func rejectInvite( cid: ChannelId, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .rejectInvite(cid: cid)) { completion?($0.error) @@ -520,7 +508,7 @@ class ChannelUpdater: Worker { /// - Parameters: /// - cid: Channel id of the channel to be marked as read /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func markRead(cid: ChannelId, userId: UserId, completion: ((Error?) -> Void)? = nil) { + func markRead(cid: ChannelId, userId: UserId, completion: (@Sendable(Error?) -> Void)? = nil) { channelRepository.markRead(cid: cid, userId: userId, completion: completion) } @@ -537,7 +525,7 @@ class ChannelUpdater: Worker { userId: UserId, from messageId: MessageId, lastReadMessageId: MessageId?, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { channelRepository.markUnread( for: cid, @@ -558,7 +546,7 @@ class ChannelUpdater: Worker { /// - cooldownDuration: Duration of the time interval users have to wait between messages. /// Specified in seconds. Should be between 0-120. Pass 0 to disable slow mode. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func enableSlowMode(cid: ChannelId, cooldownDuration: Int, completion: ((Error?) -> Void)? = nil) { + func enableSlowMode(cid: ChannelId, cooldownDuration: Int, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .enableSlowMode(cid: cid, cooldownDuration: cooldownDuration)) { completion?($0.error) } @@ -575,11 +563,11 @@ class ChannelUpdater: Worker { /// - Parameter cid: Channel id of the channel to be watched /// - Parameter isInRecoveryMode: Determines whether the SDK is in offline recovery mode /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func startWatching(cid: ChannelId, isInRecoveryMode: Bool, completion: ((Error?) -> Void)? = nil) { + func startWatching(cid: ChannelId, isInRecoveryMode: Bool, completion: (@Sendable(Error?) -> Void)? = nil) { var query = ChannelQuery(cid: cid) query.options = .all let endpoint = Endpoint.updateChannel(query: query) - let completion: (Result) -> Void = { completion?($0.error) } + let completion: @Sendable(Result) -> Void = { completion?($0.error) } if isInRecoveryMode { apiClient.recoveryRequest(endpoint: endpoint, completion: completion) } else { @@ -594,7 +582,7 @@ class ChannelUpdater: Worker { /// Please check [documentation](https://getstream.io/chat/docs/android/watch_channel/?language=swift) for more information. /// - Parameter cid: Channel id of the channel to stop watching /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func stopWatching(cid: ChannelId, completion: ((Error?) -> Void)? = nil) { + func stopWatching(cid: ChannelId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .stopWatching(cid: cid)) { completion?($0.error) } @@ -607,7 +595,7 @@ class ChannelUpdater: Worker { /// - Parameters: /// - query: Query object for watchers. See `ChannelWatcherListQuery` /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func channelWatchers(query: ChannelWatcherListQuery, completion: ((Result) -> Void)? = nil) { + func channelWatchers(query: ChannelWatcherListQuery, completion: (@Sendable(Result) -> Void)? = nil) { apiClient.request(endpoint: .channelWatchers(query: query)) { (result: Result) in do { let payload = try result.get() @@ -644,7 +632,7 @@ class ChannelUpdater: Worker { /// - freeze: Freeze or unfreeze. /// - Parameter cid: Channel id of the channel to be watched /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func freezeChannel(_ freeze: Bool, cid: ChannelId, completion: ((Error?) -> Void)? = nil) { + func freezeChannel(_ freeze: Bool, cid: ChannelId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .freezeChannel(freeze, cid: cid)) { completion?($0.error) } @@ -654,8 +642,8 @@ class ChannelUpdater: Worker { type: AttachmentType, localFileURL: URL, cid: ChannelId, - progress: ((Double) -> Void)? = nil, - completion: @escaping ((Result) -> Void) + progress: (@Sendable(Double) -> Void)? = nil, + completion: @escaping @Sendable(Result) -> Void ) { do { let attachmentFile = try AttachmentFile(url: localFileURL) @@ -679,7 +667,7 @@ class ChannelUpdater: Worker { /// Get the link attachment preview data from the provided url. /// /// This will return the data present in the OG Metadata. - public func enrichUrl(_ url: URL, completion: @escaping (Result) -> Void) { + public func enrichUrl(_ url: URL, completion: @escaping @Sendable(Result) -> Void) { apiClient.request(endpoint: .enrichUrl(url: url)) { result in switch result { case let .success(payload): @@ -700,33 +688,33 @@ class ChannelUpdater: Worker { func loadPinnedMessages( in cid: ChannelId, query: PinnedMessagesQuery, - completion: @escaping (Result<[ChatMessage], Error>) -> Void + completion: @escaping @Sendable(Result<[ChatMessage], Error>) -> Void ) { apiClient.request( endpoint: .pinnedMessages(cid: cid, query: query) ) { [weak self] result in switch result { case let .success(payload): - var pinnedMessages: [ChatMessage] = [] - self?.database.write { (session) in - pinnedMessages = session.saveMessages(messagesPayload: payload, for: cid, syncOwnReactions: false) + self?.database.write(converting: { session in + session.saveMessages(messagesPayload: payload, for: cid, syncOwnReactions: false) .compactMap { try? $0.asModel() } - } completion: { _ in - completion(.success(pinnedMessages.compactMap { $0 })) - } + }, completion: { result in + let pinnedMessages = result.value ?? [] + completion(.success(pinnedMessages)) + }) case let .failure(error): completion(.failure(error)) } } } - func deleteFile(in cid: ChannelId, url: String, completion: ((Error?) -> Void)? = nil) { + func deleteFile(in cid: ChannelId, url: String, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .deleteFile(cid: cid, url: url), completion: { completion?($0.error) }) } - func deleteImage(in cid: ChannelId, url: String, completion: ((Error?) -> Void)? = nil) { + func deleteImage(in cid: ChannelId, url: String, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .deleteImage(cid: cid, url: url), completion: { completion?($0.error) }) @@ -1004,7 +992,10 @@ extension ChannelUpdater { memberSorting: [Sorting] = [] ) async throws -> ChannelPayload { // Just populate the closure since we select the endpoint based on it. - let useCreateEndpoint: ((ChannelId) -> Void)? = channelQuery.cid == nil ? { _ in } : nil + var useCreateEndpoint: (@Sendable(ChannelId) -> Void)? + if channelQuery.cid == nil { + useCreateEndpoint = { _ in } + } return try await withCheckedThrowingContinuation { continuation in update( channelQuery: channelQuery, @@ -1036,7 +1027,7 @@ extension ChannelUpdater { type: AttachmentType, localFileURL: URL, cid: ChannelId, - progress: ((Double) -> Void)? = nil + progress: (@Sendable(Double) -> Void)? = nil ) async throws -> UploadedAttachment { try await withCheckedThrowingContinuation { continuation in uploadFile( diff --git a/Sources/StreamChat/Workers/CurrentUserUpdater.swift b/Sources/StreamChat/Workers/CurrentUserUpdater.swift index bb47067f1af..135b0e73294 100644 --- a/Sources/StreamChat/Workers/CurrentUserUpdater.swift +++ b/Sources/StreamChat/Workers/CurrentUserUpdater.swift @@ -6,7 +6,7 @@ import CoreData import Foundation /// Updates current user data to the backend and updates local storage. -class CurrentUserUpdater: Worker { +class CurrentUserUpdater: Worker, @unchecked Sendable { /// Updates the current user data. /// /// By default all data is `nil`, and it won't be updated unless a value is provided. @@ -28,7 +28,7 @@ class CurrentUserUpdater: Worker { teamsRole: [TeamId: UserRole]?, userExtraData: [String: RawJSON]?, unset: Set, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { let params: [Any?] = [name, imageURL, userExtraData] guard !params.allSatisfy({ $0 == nil }) || !unset.isEmpty else { @@ -72,7 +72,7 @@ class CurrentUserUpdater: Worker { pushProvider: PushProvider, providerName: String? = nil, currentUserId: UserId, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { database.write({ session in try session.saveCurrentDevice(deviceId) @@ -109,7 +109,7 @@ class CurrentUserUpdater: Worker { /// - currentUserId: The current user identifier. /// If `currentUser.devices` is not up-to-date, please make an `fetchDevices` call. /// - completion: Called when device is successfully deregistered, or with error. - func removeDevice(id: DeviceId, currentUserId: UserId, completion: ((Error?) -> Void)? = nil) { + func removeDevice(id: DeviceId, currentUserId: UserId, completion: (@Sendable(Error?) -> Void)? = nil) { database.write({ session in session.deleteDevice(id: id) }, completion: { databaseError in @@ -134,33 +134,28 @@ class CurrentUserUpdater: Worker { /// - Parameters: /// - currentUserId: The current user identifier. /// - completion: Called when request is successfully completed, or with error. - func fetchDevices(currentUserId: UserId, completion: ((Result<[Device], Error>) -> Void)? = nil) { + func fetchDevices(currentUserId: UserId, completion: (@Sendable(Result<[Device], Error>) -> Void)? = nil) { apiClient.request(endpoint: .devices(userId: currentUserId)) { [weak self] result in do { - var devices = [Device]() let devicesPayload = try result.get() - self?.database.write({ (session) in + self?.database.write(converting: { (session) in // Since this call always return all device, we want' to clear the existing ones // to remove the deleted devices. - devices = try session.saveCurrentUserDevices( + try session.saveCurrentUserDevices( devicesPayload.devices, clearExisting: true ) .map { try $0.asModel() } - }) { error in - if let error { - completion?(.failure(error)) - } else { - completion?(.success(devices)) - } - } + }, completion: { + completion?($0) + }) } catch { completion?(.failure(error)) } } } - func deleteAllLocalAttachmentDownloads(completion: @escaping (Error?) -> Void) { + func deleteAllLocalAttachmentDownloads(completion: @escaping @Sendable(Error?) -> Void) { database.write({ session in // Try to delete all the local files even when one of them happens to fail. var latestError: Error? @@ -186,13 +181,13 @@ class CurrentUserUpdater: Worker { /// Marks all channels for a user as read. /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func markAllRead(completion: ((Error?) -> Void)? = nil) { + func markAllRead(completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .markAllRead()) { completion?($0.error) } } - func loadAllUnreads(completion: @escaping ((Result) -> Void)) { + func loadAllUnreads(completion: @escaping (@Sendable(Result) -> Void)) { apiClient.request(endpoint: .unreads()) { result in switch result { case .success(let response): @@ -208,7 +203,7 @@ class CurrentUserUpdater: Worker { /// /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func loadBlockedUsers(completion: @escaping (Result<[BlockedUserDetails], Error>) -> Void) { + func loadBlockedUsers(completion: @escaping @Sendable(Result<[BlockedUserDetails], Error>) -> Void) { apiClient.request(endpoint: .loadBlockedUsers()) { switch $0 { case let .success(payload): diff --git a/Sources/StreamChat/Workers/EventNotificationCenter.swift b/Sources/StreamChat/Workers/EventNotificationCenter.swift index 9cda5c7c0c5..e4a56b49e65 100644 --- a/Sources/StreamChat/Workers/EventNotificationCenter.swift +++ b/Sources/StreamChat/Workers/EventNotificationCenter.swift @@ -7,7 +7,13 @@ import Foundation /// The type is designed to pre-process some incoming `Event` via middlewares before being published class EventNotificationCenter: NotificationCenter, @unchecked Sendable { - private(set) var middlewares: [EventMiddleware] = [] + private let queue = DispatchQueue(label: "io.getstream.event-notification-center-sync", target: .global()) + private(set) var middlewares: [EventMiddleware] { + get { queue.sync { _middlewares } } + set { queue.sync { _middlewares = newValue } } + } + + private var _middlewares: [EventMiddleware] = [] /// The database used when evaluating middlewares. let database: DatabaseContainer @@ -15,7 +21,12 @@ class EventNotificationCenter: NotificationCenter, @unchecked Sendable { var eventPostingQueue = DispatchQueue(label: "io.getstream.event-notification-center") // Contains the ids of the new messages that are going to be added during the ongoing process - private(set) var newMessageIds: Set = Set() + private(set) var newMessageIds: Set { + get { queue.sync { _newMessageIds } } + set { queue.sync { _newMessageIds = newValue } } + } + + private var _newMessageIds: Set = Set() init(database: DatabaseContainer) { self.database = database @@ -23,14 +34,18 @@ class EventNotificationCenter: NotificationCenter, @unchecked Sendable { } func add(middlewares: [EventMiddleware]) { - self.middlewares.append(contentsOf: middlewares) + queue.sync { + _middlewares.append(contentsOf: middlewares) + } } func add(middleware: EventMiddleware) { - middlewares.append(middleware) + queue.sync { + _middlewares.append(middleware) + } } - func process(_ events: [Event], postNotifications: Bool = true, completion: (() -> Void)? = nil) { + func process(_ events: [Event], postNotifications: Bool = true, completion: (@Sendable() -> Void)? = nil) { let processingEventsDebugMessage: () -> String = { let eventNames = events.map(\.name) return "Processing Events: \(eventNames)" @@ -41,23 +56,22 @@ class EventNotificationCenter: NotificationCenter, @unchecked Sendable { ($0 as? MessageNewEventDTO)?.message.id ?? ($0 as? NotificationMessageNewEventDTO)?.message.id } - var eventsToPost = [Event]() - database.write({ session in + database.write(converting: { session in self.newMessageIds = Set(messageIds.compactMap { !session.messageExists(id: $0) ? $0 : nil }) - eventsToPost = events.compactMap { + let eventsToPost = events.compactMap { self.middlewares.process(event: $0, session: session) } self.newMessageIds = [] - }, completion: { _ in - guard postNotifications else { + return eventsToPost + }, completion: { result in + guard let eventsToPost = result.value, postNotifications else { completion?() return } - self.eventPostingQueue.async { eventsToPost.forEach { self.post(Notification(newEventReceived: $0, sender: self)) } completion?() @@ -67,7 +81,7 @@ class EventNotificationCenter: NotificationCenter, @unchecked Sendable { } extension EventNotificationCenter { - func process(_ event: Event, postNotification: Bool = true, completion: (() -> Void)? = nil) { + func process(_ event: Event, postNotification: Bool = true, completion: (@Sendable() -> Void)? = nil) { process([event], postNotifications: postNotification, completion: completion) } } diff --git a/Sources/StreamChat/Workers/EventObservers/EventObserver.swift b/Sources/StreamChat/Workers/EventObservers/EventObserver.swift index 5114b12602a..495d7d33ead 100644 --- a/Sources/StreamChat/Workers/EventObservers/EventObserver.swift +++ b/Sources/StreamChat/Workers/EventObservers/EventObserver.swift @@ -10,8 +10,8 @@ class EventObserver { init( notificationCenter: NotificationCenter, - transform: @escaping (Event) -> EventType?, - callback: @escaping (EventType) -> Void + transform: @escaping @Sendable(Event) -> EventType?, + callback: @escaping @Sendable(EventType) -> Void ) { let observer = notificationCenter.addObserver(forName: .NewEventReceived, object: nil, queue: nil) { guard let event = $0.event.flatMap(transform) else { return } diff --git a/Sources/StreamChat/Workers/EventObservers/MemberEventObserver.swift b/Sources/StreamChat/Workers/EventObservers/MemberEventObserver.swift index 51aa344beee..9f7e3568a85 100644 --- a/Sources/StreamChat/Workers/EventObservers/MemberEventObserver.swift +++ b/Sources/StreamChat/Workers/EventObservers/MemberEventObserver.swift @@ -7,8 +7,8 @@ import Foundation final class MemberEventObserver: EventObserver { init( notificationCenter: NotificationCenter, - filter: @escaping (MemberEvent) -> Bool, - callback: @escaping (MemberEvent) -> Void + filter: @escaping @Sendable(MemberEvent) -> Bool, + callback: @escaping @Sendable(MemberEvent) -> Void ) { super.init(notificationCenter: notificationCenter, transform: { $0 as? MemberEvent }) { guard filter($0) else { return } @@ -18,7 +18,7 @@ final class MemberEventObserver: EventObserver { } extension MemberEventObserver { - convenience init(notificationCenter: NotificationCenter, callback: @escaping (MemberEvent) -> Void) { + convenience init(notificationCenter: NotificationCenter, callback: @escaping @Sendable(MemberEvent) -> Void) { self.init( notificationCenter: notificationCenter, filter: { _ in true }, @@ -26,7 +26,7 @@ extension MemberEventObserver { ) } - convenience init(notificationCenter: NotificationCenter, cid: ChannelId, callback: @escaping (MemberEvent) -> Void) { + convenience init(notificationCenter: NotificationCenter, cid: ChannelId, callback: @escaping @Sendable(MemberEvent) -> Void) { self.init( notificationCenter: notificationCenter, filter: { $0.cid == cid }, diff --git a/Sources/StreamChat/Workers/EventSender.swift b/Sources/StreamChat/Workers/EventSender.swift index ad3372df598..4b183aac150 100644 --- a/Sources/StreamChat/Workers/EventSender.swift +++ b/Sources/StreamChat/Workers/EventSender.swift @@ -5,7 +5,7 @@ import Foundation /// A worker used for sending custom events to the backend. -class EventSender: Worker { +class EventSender: Worker, @unchecked Sendable { /// Sends a custom event with the given payload to the channel with `cid`. /// /// - Parameters: @@ -15,7 +15,7 @@ class EventSender: Worker { func sendEvent( _ payload: Payload, to cid: ChannelId, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { apiClient.request(endpoint: .sendEvent(payload, cid: cid)) { completion?($0.error) diff --git a/Sources/StreamChat/Workers/MessageUpdater.swift b/Sources/StreamChat/Workers/MessageUpdater.swift index cc38c233cd0..bf6a0776452 100644 --- a/Sources/StreamChat/Workers/MessageUpdater.swift +++ b/Sources/StreamChat/Workers/MessageUpdater.swift @@ -6,7 +6,7 @@ import CoreData import Foundation /// The type provides the API for getting/editing/deleting a message -class MessageUpdater: Worker { +class MessageUpdater: Worker, @unchecked Sendable { private let repository: MessageRepository private let isLocalStorageEnabled: Bool @@ -26,7 +26,7 @@ class MessageUpdater: Worker { /// - cid: The channel identifier the message relates to. /// - messageId: The message identifier. /// - completion: The completion. Will be called with an error if something goes wrong, otherwise - will be called with `nil`. - func getMessage(cid: ChannelId, messageId: MessageId, completion: ((Result) -> Void)? = nil) { + func getMessage(cid: ChannelId, messageId: MessageId, completion: (@Sendable(Result) -> Void)? = nil) { repository.getMessage(cid: cid, messageId: messageId, store: true, completion: completion) } @@ -43,17 +43,16 @@ class MessageUpdater: Worker { /// - messageId: The message identifier. /// - hard: A Boolean value to determine if the message will be delete permanently on the backend. /// - completion: The completion. Will be called with an error if smth goes wrong, otherwise - will be called with `nil`. - func deleteMessage(messageId: MessageId, hard: Bool, completion: ((Error?) -> Void)? = nil) { - var shouldDeleteOnBackend = true - - database.write({ session in + func deleteMessage(messageId: MessageId, hard: Bool, completion: (@Sendable(Error?) -> Void)? = nil) { + database.write(converting: { session in + var shouldDeleteOnBackend = true guard let messageDTO = session.message(id: messageId) else { // Even though the message does not exist locally // we don't throw any error because we still want // to try to delete the message on the backend. - return + return shouldDeleteOnBackend } - + // Hard Deleting is necessary for messages which are only available locally in the DB // or if we want to explicitly hard delete the message with hard == true. let shouldBeHardDeleted = hard || messageDTO.isLocalOnly @@ -74,22 +73,27 @@ class MessageUpdater: Worker { } else { messageDTO.localMessageState = .deleting } - }, completion: { [weak database, weak apiClient, weak repository] error in - guard shouldDeleteOnBackend, error == nil else { + return shouldDeleteOnBackend + }, completion: { [weak database, weak apiClient, weak repository] result in + switch result { + case .failure(let error): completion?(error) - return - } - - apiClient?.request(endpoint: .deleteMessage(messageId: messageId, hard: hard)) { result in - switch result { - case let .success(response): - repository?.saveSuccessfullyDeletedMessage(message: response.message, completion: completion) - case let .failure(error): - database?.write { session in - let messageDTO = session.message(id: messageId) - messageDTO?.localMessageState = .deletingFailed - messageDTO?.isHardDeleted = false - completion?(error) + case .success(let shouldDeleteOnBackend): + if !shouldDeleteOnBackend { + completion?(nil) + return + } + apiClient?.request(endpoint: .deleteMessage(messageId: messageId, hard: hard)) { [weak database, weak repository] result in + switch result { + case let .success(response): + repository?.saveSuccessfullyDeletedMessage(message: response.message, completion: completion) + case let .failure(error): + database?.write { session in + let messageDTO = session.message(id: messageId) + messageDTO?.localMessageState = .deletingFailed + messageDTO?.isHardDeleted = false + completion?(error) + } } } } @@ -111,10 +115,9 @@ class MessageUpdater: Worker { skipEnrichUrl: Bool, attachments: [AnyAttachmentPayload] = [], extraData: [String: RawJSON]? = nil, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { - var message: ChatMessage? - database.write({ session in + database.write(converting: { session in let messageDTO = try session.messageEditableByCurrentUser(messageId) func updateMessage(localState: LocalMessageState) throws { @@ -154,8 +157,7 @@ class MessageUpdater: Worker { if messageDTO.isBounced { try updateMessage(localState: .pendingSend) - message = try messageDTO.asModel() - return + return try messageDTO.asModel() } switch messageDTO.localMessageState { @@ -169,15 +171,9 @@ class MessageUpdater: Worker { reason: "message is in `\(messageDTO.localMessageState!)` state" ) } - message = try messageDTO.asModel() - }, completion: { error in - if let error { - completion?(.failure(error)) - } else if let message { - completion?(.success(message)) - } else { - completion?(.failure(ClientError.MessageDoesNotExist(messageId: messageId))) - } + return try messageDTO.asModel() + }, completion: { + completion?($0) }) } @@ -214,10 +210,9 @@ class MessageUpdater: Worker { skipPush: Bool, skipEnrichUrl: Bool, extraData: [String: RawJSON], - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { - var newMessage: ChatMessage? - database.write({ (session) in + database.write(converting: { (session) in let newMessageDTO = try session.createNewMessage( in: cid, messageId: messageId, @@ -242,15 +237,10 @@ class MessageUpdater: Worker { newMessageDTO.showInsideThread = true newMessageDTO.localMessageState = .pendingSend - newMessage = try newMessageDTO.asModel() - - }) { error in - if let message = newMessage, error == nil { - completion?(.success(message)) - } else { - completion?(.failure(error ?? ClientError.Unknown())) - } - } + return try newMessageDTO.asModel() + }, completion: { + completion?($0) + }) } /// Loads replies for the given message. @@ -265,7 +255,7 @@ class MessageUpdater: Worker { messageId: MessageId, pagination: MessagesPagination, paginationStateHandler: MessagesPaginationStateHandling, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { paginationStateHandler.begin(pagination: pagination) @@ -312,7 +302,7 @@ class MessageUpdater: Worker { cid: ChannelId, messageId: MessageId, pagination: Pagination, - completion: ((Result<[ChatMessageReaction], Error>) -> Void)? = nil + completion: (@Sendable(Result<[ChatMessageReaction], Error>) -> Void)? = nil ) { let endpoint: Endpoint = .loadReactions( messageId: messageId, @@ -322,15 +312,10 @@ class MessageUpdater: Worker { apiClient.request(endpoint: endpoint) { result in switch result { case let .success(payload): - var reactions: [ChatMessageReaction] = [] - self.database.write({ session in - reactions = try session.saveReactions(payload: payload, query: nil).map { try $0.asModel() } - }, completion: { error in - if let error = error { - completion?(.failure(error)) - } else { - completion?(.success(reactions)) - } + self.database.write(converting: { session in + try session.saveReactions(payload: payload, query: nil).map { try $0.asModel() } + }, completion: { + completion?($0) }) case let .failure(error): completion?(.failure(error)) @@ -355,7 +340,7 @@ class MessageUpdater: Worker { in cid: ChannelId, reason: String? = nil, extraData: [String: RawJSON]? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { fetchAndSaveMessageIfNeeded(messageId, cid: cid) { error in guard error == nil else { @@ -402,7 +387,7 @@ class MessageUpdater: Worker { enforceUnique: Bool, extraData: [String: RawJSON], messageId: MessageId, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { let version = UUID().uuidString @@ -429,7 +414,7 @@ class MessageUpdater: Worker { log.warning("Failed to optimistically add the reaction to the database: \(error)") } } completion: { [weak self, weak repository] error in - self?.apiClient.request(endpoint: endpoint) { result in + self?.apiClient.request(endpoint: endpoint) { [weak self, weak repository] result in guard let error = result.error else { return } if self?.canKeepReactionState(for: error) == true { return } @@ -448,27 +433,30 @@ class MessageUpdater: Worker { func deleteReaction( _ type: MessageReactionType, messageId: MessageId, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { - var reactionScore: Int? - database.write { session in + database.write(converting: { session in + var reactionScore: Int? do { - guard let reaction = try session.removeReaction(from: messageId, type: type, on: nil) else { return } - reaction.localState = .pendingDelete - reactionScore = Int(reaction.score) + if let reaction = try session.removeReaction(from: messageId, type: type, on: nil) { + reaction.localState = .pendingDelete + reactionScore = Int(reaction.score) + } } catch { log.warning("Failed to remove the reaction from to the database: \(error)") } - } completion: { [weak self, weak repository] error in - self?.apiClient.request(endpoint: .deleteReaction(type, messageId: messageId)) { result in + return reactionScore + }, completion: { [weak self, weak repository] writeResult in + let reactionScore = writeResult.value ?? nil + self?.apiClient.request(endpoint: .deleteReaction(type, messageId: messageId)) { [weak self, weak repository] result in guard let error = result.error else { return } if self?.canKeepReactionState(for: error) == true { return } repository?.undoReactionDeletion(on: messageId, type: type, score: reactionScore ?? 1) } - completion?(error) - } + completion?(writeResult.error) + }) } private func canKeepReactionState(for error: Error) -> Bool { @@ -479,7 +467,7 @@ class MessageUpdater: Worker { /// - Parameters: /// - messageId: The message identifier. /// - pinning: The pinning expiration information. It supports setting an infinite expiration, setting a date, or the amount of time a message is pinned. - func pinMessage(messageId: MessageId, pinning: MessagePinning, completion: ((Result) -> Void)? = nil) { + func pinMessage(messageId: MessageId, pinning: MessagePinning, completion: (@Sendable(Result) -> Void)? = nil) { pinLocalMessage(on: messageId, pinning: pinning) { [weak self] pinResult in switch pinResult { case .failure(let pinError): @@ -490,7 +478,7 @@ class MessageUpdater: Worker { request: .init(set: .init(pinned: true)) ) - self?.apiClient.request(endpoint: endpoint) { result in + self?.apiClient.request(endpoint: endpoint) { [weak self] result in switch result { case .success: completion?(.success(message)) @@ -508,7 +496,7 @@ class MessageUpdater: Worker { /// - Parameters: /// - messageId: The message identifier. /// - completion: The completion handler with the result. - func unpinMessage(messageId: MessageId, completion: ((Result) -> Void)? = nil) { + func unpinMessage(messageId: MessageId, completion: (@Sendable(Result) -> Void)? = nil) { unpinLocalMessage(on: messageId) { [weak self] unpinResult, pinning in switch unpinResult { case .failure(let unpinError): @@ -519,7 +507,7 @@ class MessageUpdater: Worker { request: .init(set: .init(pinned: false)) ) - self?.apiClient.request(endpoint: endpoint) { result in + self?.apiClient.request(endpoint: endpoint) { [weak self] result in switch result { case .success: completion?(.success(message)) @@ -536,53 +524,46 @@ class MessageUpdater: Worker { private func pinLocalMessage( on messageId: MessageId, pinning: MessagePinning, - completion: ((Result) -> Void)? = nil + completion: (@Sendable(Result) -> Void)? = nil ) { - var message: ChatMessage! - database.write { session in + database.write(converting: { session in guard let messageDTO = session.message(id: messageId) else { throw ClientError.MessageDoesNotExist(messageId: messageId) } try session.pin(message: messageDTO, pinning: pinning) - message = try messageDTO.asModel() - } completion: { error in - if let error = error { - log.error("Error pinning the message with id \(messageId): \(error)") - completion?(.failure(error)) - } else { - completion?(.success(message)) - } - } + return try messageDTO.asModel() + }, completion: { + completion?($0) + }) } private func unpinLocalMessage( on messageId: MessageId, - completion: ((Result, MessagePinning) -> Void)? = nil + completion: (@Sendable(Result, MessagePinning) -> Void)? = nil ) { - var message: ChatMessage! - var pinning: MessagePinning = .noExpiration - database.write { session in + database.write(converting: { session in guard let messageDTO = session.message(id: messageId) else { throw ClientError.MessageDoesNotExist(messageId: messageId) } - pinning = .init(expirationDate: messageDTO.pinExpires?.bridgeDate) + let pinning = MessagePinning(expirationDate: messageDTO.pinExpires?.bridgeDate) session.unpin(message: messageDTO) - message = try messageDTO.asModel() - } completion: { error in - if let error = error { - log.error("Error unpinning the message with id \(messageId): \(error)") - completion?(.failure(error), pinning) - } else { - completion?(.success(message), pinning) + let message = try messageDTO.asModel() + return (message: message, pinning: pinning) + }, completion: { result in + switch result { + case .success(let messageAndPinning): + completion?(.success(messageAndPinning.message), messageAndPinning.pinning) + case .failure(let error): + completion?(.failure(error), .noExpiration) } - } + }) } static let minSignificantDownloadingProgressChange: Double = 0.01 func downloadAttachment( _ attachment: ChatMessageAttachment, - completion: @escaping (Result, Error>) -> Void + completion: @escaping @Sendable(Result, Error>) -> Void ) where Payload: DownloadableAttachmentPayload { let attachmentId = attachment.id let localURL = URL.streamAttachmentLocalStorageURL(forRelativePath: attachment.relativeStoragePath) @@ -615,7 +596,7 @@ class MessageUpdater: Worker { ) } - func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: @escaping (Error?) -> Void) { + func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: @escaping @Sendable(Error?) -> Void) { database.write({ session in let dto = session.attachment(id: attachmentId) guard let attachment = dto?.asAnyModel() else { @@ -634,10 +615,9 @@ class MessageUpdater: Worker { payloadType: Payload.Type, newState: LocalAttachmentDownloadState, localURL: URL, - completion: ((Result, Error>) -> Void)? = nil + completion: (@Sendable(Result, Error>) -> Void)? = nil ) where Payload: DownloadableAttachmentPayload { - var model: ChatMessageAttachment? - database.write({ session in + database.write(converting: { session in guard let attachmentDTO = session.attachment(id: attachmentId) else { throw ClientError.AttachmentDoesNotExist(id: attachmentId) } @@ -649,25 +629,20 @@ class MessageUpdater: Worker { return attachmentDTO.localDownloadState != newState } }() - guard needsUpdate else { return } - attachmentDTO.localDownloadState = newState - // Store only the relative path because sandboxed base URL can change between app launchs - attachmentDTO.localRelativePath = localURL.relativePath - - guard completion != nil else { return } + if needsUpdate { + attachmentDTO.localDownloadState = newState + // Store only the relative path because sandboxed base URL can change between app launchs + attachmentDTO.localRelativePath = localURL.relativePath + } guard let attachmentAnyModel = attachmentDTO.asAnyModel() else { throw ClientError.AttachmentDoesNotExist(id: attachmentId) } guard let result = attachmentAnyModel.attachment(payloadType: Payload.self) else { throw ClientError.AttachmentDownloading(id: attachmentId, reason: "Invalid payload type: \(Payload.self)") } - model = result - }, completion: { error in - if let error { - completion?(.failure(error)) - } else if let model { - completion?(.success(model)) - } + return result + }, completion: { + completion?($0) }) } @@ -677,7 +652,7 @@ class MessageUpdater: Worker { /// - completion: Called when the attachment database entity is updated. Called with `Error` if update fails. func restartFailedAttachmentUploading( with id: AttachmentId, - completion: @escaping (Error?) -> Void + completion: @escaping @Sendable(Error?) -> Void ) { database.write({ guard let attachmentDTO = $0.attachment(id: id) else { @@ -701,8 +676,7 @@ class MessageUpdater: Worker { /// - completion: Called when the message database entity is updated. Called with `Error` if update fails. func resendMessage( with messageId: MessageId, - completion: @escaping (Error? - ) -> Void + completion: @escaping @Sendable(Error?) -> Void ) { database.write({ let messageDTO = try $0.messageEditableByCurrentUser(messageId) @@ -733,7 +707,7 @@ class MessageUpdater: Worker { cid: ChannelId, messageId: MessageId, action: AttachmentAction, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { database.write({ session in let messageDTO = try session.messageEditableByCurrentUser(messageId) @@ -784,35 +758,29 @@ class MessageUpdater: Worker { }) } - func search(query: MessageSearchQuery, policy: UpdatePolicy = .merge, completion: ((Result) -> Void)? = nil) { + func search(query: MessageSearchQuery, policy: UpdatePolicy = .merge, completion: (@Sendable(Result) -> Void)? = nil) { apiClient.request(endpoint: .search(query: query)) { result in switch result { case let .success(payload): - var messages = [ChatMessage]() - self.database.write { session in + self.database.write(converting: { session in if case .replace = policy { let dto = session.saveQuery(query: query) dto.messages.removeAll() } let dtos = session.saveMessageSearch(payload: payload, for: query) - if completion != nil { - messages = try dtos.map { try $0.asModel() } - } - } completion: { error in - if let error = error { - completion?(.failure(error)) - } else { - completion?(.success(MessageSearchResults(payload: payload, models: messages))) - } - } + let messages = try dtos.map { try $0.asModel() } + return MessageSearchResults(payload: payload, models: messages) + }, completion: { + completion?($0) + }) case let .failure(error): completion?(.failure(error)) } } } - func clearSearchResults(for query: MessageSearchQuery, completion: ((Error?) -> Void)? = nil) { + func clearSearchResults(for query: MessageSearchQuery, completion: (@Sendable(Error?) -> Void)? = nil) { database.write { session in let dto = session.saveQuery(query: query) dto.messages.removeAll() @@ -821,12 +789,11 @@ class MessageUpdater: Worker { } } - func translate(messageId: MessageId, to language: TranslationLanguage, completion: ((Result) -> Void)? = nil) { + func translate(messageId: MessageId, to language: TranslationLanguage, completion: (@Sendable(Result) -> Void)? = nil) { apiClient.request(endpoint: .translate(messageId: messageId, to: language), completion: { result in switch result { case let .success(boxedMessage): - var translatedMessage: ChatMessage? - self.database.write { session in + self.database.write(converting: { session in let messageDTO = try session.saveMessage( payload: boxedMessage.message, for: boxedMessage.message.cid, @@ -834,16 +801,10 @@ class MessageUpdater: Worker { skipDraftUpdate: true, cache: nil ) - if completion != nil { - translatedMessage = try messageDTO.asModel() - } - } completion: { error in - if let translatedMessage, error == nil { - completion?(.success(translatedMessage)) - } else { - completion?(.failure(error ?? ClientError.Unknown())) - } - } + return try messageDTO.asModel() + }, completion: { + completion?($0) + }) case let .failure(error): completion?(.failure(error)) } @@ -853,7 +814,7 @@ class MessageUpdater: Worker { func markThreadRead( cid: ChannelId, threadId: MessageId, - completion: @escaping ((Error?) -> Void) + completion: @escaping (@Sendable(Error?) -> Void) ) { apiClient.request( endpoint: .markThreadRead(cid: cid, threadId: threadId) @@ -865,7 +826,7 @@ class MessageUpdater: Worker { func markThreadUnread( cid: ChannelId, threadId: MessageId, - completion: @escaping ((Error?) -> Void) + completion: @escaping (@Sendable(Error?) -> Void) ) { apiClient.request( endpoint: .markThreadUnread(cid: cid, threadId: threadId) @@ -874,7 +835,7 @@ class MessageUpdater: Worker { } } - func loadThread(query: ThreadQuery, completion: @escaping ((Result) -> Void)) { + func loadThread(query: ThreadQuery, completion: @escaping @Sendable(Result) -> Void) { apiClient.request(endpoint: .thread(query: query)) { result in switch result { case .success(let response): @@ -891,7 +852,7 @@ class MessageUpdater: Worker { func updateThread( for messageId: MessageId, request: ThreadPartialUpdateRequest, - completion: @escaping ((Result) -> Void) + completion: @escaping @Sendable(Result) -> Void ) { apiClient.request( endpoint: .partialThreadUpdate( @@ -923,7 +884,7 @@ extension MessageUpdater { // MARK: - Private private extension MessageUpdater { - func fetchAndSaveMessageIfNeeded(_ messageId: MessageId, cid: ChannelId, completion: @escaping (Error?) -> Void) { + func fetchAndSaveMessageIfNeeded(_ messageId: MessageId, cid: ChannelId, completion: @escaping @Sendable(Error?) -> Void) { checkMessageExistsLocally(messageId) { exists in exists ? completion(nil) : self.getMessage( cid: cid, @@ -943,13 +904,13 @@ private extension MessageUpdater { } extension ClientError { - final class MessageDoesNotExist: ClientError { + final class MessageDoesNotExist: ClientError, @unchecked Sendable { init(messageId: MessageId) { super.init("There is no `MessageDTO` instance in the DB matching id: \(messageId).") } } - final class MessageEditing: ClientError { + final class MessageEditing: ClientError, @unchecked Sendable { init(messageId: String, reason: String) { super.init("Message with id: \(messageId) can't be edited (\(reason)") } diff --git a/Sources/StreamChat/Workers/ReactionListUpdater.swift b/Sources/StreamChat/Workers/ReactionListUpdater.swift index d1476ee51d3..8731b5280f6 100644 --- a/Sources/StreamChat/Workers/ReactionListUpdater.swift +++ b/Sources/StreamChat/Workers/ReactionListUpdater.swift @@ -4,26 +4,19 @@ import CoreData -class ReactionListUpdater: Worker { +class ReactionListUpdater: Worker, @unchecked Sendable { func loadReactions( query: ReactionListQuery, - completion: @escaping (Result<[ChatMessageReaction], Error>) -> Void + completion: @escaping @Sendable(Result<[ChatMessageReaction], Error>) -> Void ) { apiClient.request( endpoint: .loadReactionsV2(query: query) ) { [weak self] (result: Result) in switch result { case let .success(payload): - var reactions: [ChatMessageReaction] = [] - self?.database.write({ session in - reactions = try session.saveReactions(payload: payload, query: query).map { try $0.asModel() } - }, completion: { error in - if let error = error { - completion(.failure(error)) - } else { - completion(.success(reactions)) - } - }) + self?.database.write(converting: { session in + try session.saveReactions(payload: payload, query: query).map { try $0.asModel() } + }, completion: completion) case let .failure(error): completion(.failure(error)) } diff --git a/Sources/StreamChat/Workers/ReadStateHandler.swift b/Sources/StreamChat/Workers/ReadStateHandler.swift index 8020e903380..1bce7e8a1e2 100644 --- a/Sources/StreamChat/Workers/ReadStateHandler.swift +++ b/Sources/StreamChat/Workers/ReadStateHandler.swift @@ -7,7 +7,7 @@ import Foundation /// A handler which enables marking channels read and unread. /// /// Only one mark read or unread request is allowed to be active. -final class ReadStateHandler { +final class ReadStateHandler: @unchecked Sendable { private let authenticationRepository: AuthenticationRepository private let channelUpdater: ChannelUpdater private let messageRepository: MessageRepository @@ -24,7 +24,7 @@ final class ReadStateHandler { self.messageRepository = messageRepository } - func markRead(_ channel: ChatChannel, completion: @escaping (Error?) -> Void) { + func markRead(_ channel: ChatChannel, completion: @escaping @Sendable(Error?) -> Void) { guard !markingRead, let currentUserId = authenticationRepository.currentUserId, @@ -54,7 +54,7 @@ final class ReadStateHandler { func markUnread( from messageId: MessageId, in channel: ChatChannel, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { guard !markingRead, let currentUserId = authenticationRepository.currentUserId diff --git a/Sources/StreamChat/Workers/TypingEventsSender.swift b/Sources/StreamChat/Workers/TypingEventsSender.swift index f8f6761cf05..b814718741c 100644 --- a/Sources/StreamChat/Workers/TypingEventsSender.swift +++ b/Sources/StreamChat/Workers/TypingEventsSender.swift @@ -20,7 +20,7 @@ private struct TypingInfo { } /// Sends typing events. -class TypingEventsSender: Worker { +class TypingEventsSender: Worker, @unchecked Sendable { /// A timer type. var timer: Timer.Type = DefaultTimer.self /// `TypingInfo` for channel (and parent message) that typing has occurred in. Stored to stop typing when `TypingEventsSender` is deallocated @@ -42,7 +42,7 @@ class TypingEventsSender: Worker { // MARK: Typing events - func keystroke(in cid: ChannelId, parentMessageId: MessageId?, completion: ((Error?) -> Void)? = nil) { + func keystroke(in cid: ChannelId, parentMessageId: MessageId?, completion: (@Sendable(Error?) -> Void)? = nil) { cancelScheduledTypingTimerControl() currentUserTypingTimerControl = timer.schedule(timeInterval: .startTypingEventTimeout, queue: .main) { [weak self] in @@ -60,7 +60,7 @@ class TypingEventsSender: Worker { startTyping(in: cid, parentMessageId: parentMessageId, completion: completion) } - func startTyping(in cid: ChannelId, parentMessageId: MessageId?, completion: ((Error?) -> Void)? = nil) { + func startTyping(in cid: ChannelId, parentMessageId: MessageId?, completion: (@Sendable(Error?) -> Void)? = nil) { typingInfo = .init(channelId: cid, parentMessageId: parentMessageId) currentUserLastTypingDate = timer.currentTime() @@ -71,7 +71,7 @@ class TypingEventsSender: Worker { } } - func stopTyping(in cid: ChannelId, parentMessageId: MessageId?, completion: ((Error?) -> Void)? = nil) { + func stopTyping(in cid: ChannelId, parentMessageId: MessageId?, completion: (@Sendable(Error?) -> Void)? = nil) { // If there's a timer set, we clear it if currentUserLastTypingDate != nil { cancelScheduledTypingTimerControl() diff --git a/Sources/StreamChat/Workers/UserListUpdater.swift b/Sources/StreamChat/Workers/UserListUpdater.swift index 15b1c01d94b..a12fd3cc4b1 100644 --- a/Sources/StreamChat/Workers/UserListUpdater.swift +++ b/Sources/StreamChat/Workers/UserListUpdater.swift @@ -5,7 +5,7 @@ import CoreData /// Makes a users query call to the backend and updates the local storage with the results. -class UserListUpdater: Worker { +class UserListUpdater: Worker, @unchecked Sendable { /// Makes a users query call to the backend and updates the local storage with the results. /// /// - Parameters: @@ -13,29 +13,21 @@ class UserListUpdater: Worker { /// - policy: The update policy for the resulting user set. See `UpdatePolicy` /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func update(userListQuery: UserListQuery, policy: UpdatePolicy = .merge, completion: ((Result<[ChatUser], Error>) -> Void)? = nil) { + func update(userListQuery: UserListQuery, policy: UpdatePolicy = .merge, completion: (@Sendable(Result<[ChatUser], Error>) -> Void)? = nil) { fetch(userListQuery: userListQuery) { [weak self] (result: Result) in switch result { case let .success(userListPayload): - var users = [ChatUser]() - self?.database.write { session in + self?.database.write(converting: { session in if case .replace = policy { let dto = try session.saveQuery(query: userListQuery) dto?.users.removeAll() } - + let dtos = session.saveUsers(payload: userListPayload, query: userListQuery) - if completion != nil { - users = try dtos.map { try $0.asModel() } - } - } completion: { error in - if let error = error { - log.error("Failed to save `UserListPayload` to the database. Error: \(error)") - completion?(.failure(error)) - } else { - completion?(.success(users)) - } - } + return try dtos.map { try $0.asModel() } + }, completion: { + completion?($0) + }) case let .failure(error): completion?(.failure(error)) } @@ -50,7 +42,7 @@ class UserListUpdater: Worker { /// func fetch( userListQuery: UserListQuery, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable(Result) -> Void ) { apiClient.request( endpoint: .users(query: userListQuery), diff --git a/Sources/StreamChat/Workers/UserUpdater.swift b/Sources/StreamChat/Workers/UserUpdater.swift index ad703fd8741..0995d922a38 100644 --- a/Sources/StreamChat/Workers/UserUpdater.swift +++ b/Sources/StreamChat/Workers/UserUpdater.swift @@ -6,13 +6,13 @@ import CoreData import Foundation /// Makes user-related calls to the backend and updates the local storage with the results. -class UserUpdater: Worker { +class UserUpdater: Worker, @unchecked Sendable { /// Mutes the user with the provided `userId`. /// - Parameters: /// - userId: The user identifier. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func muteUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + func muteUser(_ userId: UserId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .muteUser(userId)) { completion?($0.error) } @@ -23,7 +23,7 @@ class UserUpdater: Worker { /// - userId: The user identifier. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func unmuteUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + func unmuteUser(_ userId: UserId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .unmuteUser(userId)) { completion?($0.error) } @@ -34,7 +34,7 @@ class UserUpdater: Worker { /// - userId: The user identifier. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func blockUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + func blockUser(_ userId: UserId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .blockUser(userId)) { switch $0 { case .success: @@ -65,7 +65,7 @@ class UserUpdater: Worker { /// - userId: The user identifier. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func unblockUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + func unblockUser(_ userId: UserId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient.request(endpoint: .unblockUser(userId)) { switch $0 { case .success: @@ -97,7 +97,7 @@ class UserUpdater: Worker { /// - userId: The user identifier /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. /// - func loadUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + func loadUser(_ userId: UserId, completion: (@Sendable(Error?) -> Void)? = nil) { apiClient .request(endpoint: .users(query: .user(withID: userId))) { (result: Result) in switch result { @@ -140,7 +140,7 @@ class UserUpdater: Worker { with userId: UserId, reason: String? = nil, extraData: [String: RawJSON]? = nil, - completion: ((Error?) -> Void)? = nil + completion: (@Sendable(Error?) -> Void)? = nil ) { let endpoint: Endpoint = .flagUser( flag, @@ -174,7 +174,7 @@ class UserUpdater: Worker { } extension ClientError { - final class UserDoesNotExist: ClientError { + final class UserDoesNotExist: ClientError, @unchecked Sendable { init(userId: UserId) { super.init("There is no user with id: <\(userId)>.") } diff --git a/Sources/StreamChat/Workers/Worker.swift b/Sources/StreamChat/Workers/Worker.swift index 710fbc333ba..214e8c18af4 100644 --- a/Sources/StreamChat/Workers/Worker.swift +++ b/Sources/StreamChat/Workers/Worker.swift @@ -10,7 +10,7 @@ typealias WorkerBuilder = ( _ apiClient: APIClient ) -> Worker -class Worker { +class Worker: @unchecked Sendable { let database: DatabaseContainer let apiClient: APIClient diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec index 044c35d1bc8..100adcf2179 100644 --- a/StreamChat-XCFramework.podspec +++ b/StreamChat-XCFramework.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |spec| spec.author = { "getstream.io" => "support@getstream.io" } spec.social_media_url = "https://getstream.io" - spec.swift_version = '5.7' + spec.swift_version = '6.0' spec.ios.deployment_target = '13.0' spec.requires_arc = true diff --git a/StreamChat.podspec b/StreamChat.podspec index 8e193633a9f..641676b1c3e 100644 --- a/StreamChat.podspec +++ b/StreamChat.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |spec| spec.author = { "getstream.io" => "support@getstream.io" } spec.social_media_url = "https://getstream.io" - spec.swift_version = '5.7' + spec.swift_version = '6.0' spec.ios.deployment_target = '13.0' spec.osx.deployment_target = '11.0' spec.requires_arc = true diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index b2d9a14d7c5..85ad2389c78 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -281,6 +281,8 @@ 4F73F3992B91BD3000563CD9 /* MessageState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F73F3972B91BD3000563CD9 /* MessageState.swift */; }; 4F73F39E2B91C7BF00563CD9 /* MessageState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F73F39D2B91C7BF00563CD9 /* MessageState+Observer.swift */; }; 4F73F39F2B91C7BF00563CD9 /* MessageState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F73F39D2B91C7BF00563CD9 /* MessageState+Observer.swift */; }; + 4F7B58952DCB6B5A0034CC0F /* AllocatedUnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7B58942DCB6B5A0034CC0F /* AllocatedUnfairLock.swift */; }; + 4F7B58962DCB6B5A0034CC0F /* AllocatedUnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7B58942DCB6B5A0034CC0F /* AllocatedUnfairLock.swift */; }; 4F83FA462BA43DC3008BD8CD /* MemberList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F83FA452BA43DC3008BD8CD /* MemberList.swift */; }; 4F83FA472BA43DC3008BD8CD /* MemberList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F83FA452BA43DC3008BD8CD /* MemberList.swift */; }; 4F862F9A2C38001000062502 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F862F992C38001000062502 /* FileManager+Extensions.swift */; }; @@ -340,6 +342,8 @@ 4FE6E1AB2BAC79F400C80AF1 /* MemberListState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6E1A92BAC79F400C80AF1 /* MemberListState+Observer.swift */; }; 4FE6E1AD2BAC7A1B00C80AF1 /* UserListState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6E1AC2BAC7A1B00C80AF1 /* UserListState+Observer.swift */; }; 4FE6E1AE2BAC7A1B00C80AF1 /* UserListState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6E1AC2BAC7A1B00C80AF1 /* UserListState+Observer.swift */; }; + 4FE913872D82C8ED0048AC16 /* BoxedAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE913862D82C8E90048AC16 /* BoxedAny.swift */; }; + 4FE913882D82C8ED0048AC16 /* BoxedAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE913862D82C8E90048AC16 /* BoxedAny.swift */; }; 4FF2A80D2B8E011000941A64 /* ChatState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF2A80C2B8E011000941A64 /* ChatState+Observer.swift */; }; 4FF2A80E2B8E011000941A64 /* ChatState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF2A80C2B8E011000941A64 /* ChatState+Observer.swift */; }; 4FF9B2682C6F697300A3B711 /* AttachmentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF9B2672C6F696B00A3B711 /* AttachmentDownloader.swift */; }; @@ -3221,6 +3225,7 @@ 4F6B840F2D008D5F005645B0 /* MemberUpdatePayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberUpdatePayload.swift; sourceTree = ""; }; 4F73F3972B91BD3000563CD9 /* MessageState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageState.swift; sourceTree = ""; }; 4F73F39D2B91C7BF00563CD9 /* MessageState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageState+Observer.swift"; sourceTree = ""; }; + 4F7B58942DCB6B5A0034CC0F /* AllocatedUnfairLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllocatedUnfairLock.swift; sourceTree = ""; }; 4F83FA452BA43DC3008BD8CD /* MemberList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberList.swift; sourceTree = ""; }; 4F862F992C38001000062502 /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = ""; }; 4F877D392D019E0400CB66EC /* ChannelPinningScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPinningScope.swift; sourceTree = ""; }; @@ -3254,6 +3259,7 @@ 4FE56B8F2D5E002300589F9A /* MarkdownParser_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownParser_Tests.swift; sourceTree = ""; }; 4FE6E1A92BAC79F400C80AF1 /* MemberListState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MemberListState+Observer.swift"; sourceTree = ""; }; 4FE6E1AC2BAC7A1B00C80AF1 /* UserListState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserListState+Observer.swift"; sourceTree = ""; }; + 4FE913862D82C8E90048AC16 /* BoxedAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxedAny.swift; sourceTree = ""; }; 4FF2A80C2B8E011000941A64 /* ChatState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatState+Observer.swift"; sourceTree = ""; }; 4FF9B2672C6F696B00A3B711 /* AttachmentDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDownloader.swift; sourceTree = ""; }; 4FFB5E9F2BA0507900F0454F /* Collection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Extensions.swift"; sourceTree = ""; }; @@ -5622,8 +5628,10 @@ ADE2093B29FBEC88007D0FF3 /* MessagesPaginationStateHandling */, C1E8AD5A278DDEBB0041B775 /* Operations */, CF324E762832FC1200E5BBE6 /* StreamTimer */, + 4F7B58942DCB6B5A0034CC0F /* AllocatedUnfairLock.swift */, 40D3962D2A0910DE0020DDC9 /* Array+Sampling.swift */, 79280F6F2487CD2B00CDEB89 /* Atomic.swift */, + 4FE913862D82C8E90048AC16 /* BoxedAny.swift */, 797A756724814F0D003CF16D /* Bundle+Extensions.swift */, 792A4F3D247FFDE700EAF71D /* Codable+Extensions.swift */, CF6E489E282341F2008416DC /* CountdownTracker.swift */, @@ -6693,10 +6701,10 @@ A3227E54284A479700EBE6CC /* Extensions */ = { isa = PBXGroup; children = ( + A3227E75284A4C6400EBE6CC /* MessageReactionType+Position.swift */, A3227E58284A484300EBE6CC /* UIImage+Resized.swift */, A3227E5A284A489000EBE6CC /* UIViewController+Alert.swift */, 849C1B69283686EE00F9DC42 /* UserDefaults+Shared.swift */, - A3227E75284A4C6400EBE6CC /* MessageReactionType+Position.swift */, ); path = Extensions; sourceTree = ""; @@ -11500,6 +11508,7 @@ ADB951B2291C3CE900800554 /* AnyAttachmentUpdater.swift in Sources */, 79617CB125F236B600D54E61 /* UserWatchingEventMiddleware.swift in Sources */, ADB951A1291BD7CC00800554 /* UploadedAttachment.swift in Sources */, + 4FE913872D82C8ED0048AC16 /* BoxedAny.swift in Sources */, 7962958C248147430078EB53 /* BaseURL.swift in Sources */, 8802F9EF25AF3D4200475159 /* HTTPHeader.swift in Sources */, 8A0C3BD424C1DF2100CAFD19 /* MessageEvents.swift in Sources */, @@ -11538,6 +11547,7 @@ 797A756824814F0D003CF16D /* Bundle+Extensions.swift in Sources */, 7908829C2546D95A00896F03 /* FlagMessagePayload.swift in Sources */, DA0BB1612513B5F200CAEFBD /* StringInterpolation+Extensions.swift in Sources */, + 4F7B58952DCB6B5A0034CC0F /* AllocatedUnfairLock.swift in Sources */, 64C8C86E26934C6100329F82 /* UserInfo.swift in Sources */, C1FFD9F927ECC7C7008A6848 /* Filter+ChatChannel.swift in Sources */, 4F1BEE7C2BE3851200B6685C /* ReactionListState+Observer.swift in Sources */, @@ -12306,6 +12316,7 @@ C121E82A274544AD00023E4C /* ConnectionStatus.swift in Sources */, C121E82B274544AD00023E4C /* APIPathConvertible.swift in Sources */, AD8FEE5C2AA8E1E400273F88 /* ChatClientFactory.swift in Sources */, + 4F7B58962DCB6B5A0034CC0F /* AllocatedUnfairLock.swift in Sources */, 40789D2E29F6AC500018C2BB /* AudioRecordingState.swift in Sources */, C14D27B72869EEE40063F6F2 /* Sequence+CompactMapLoggingError.swift in Sources */, C121E82C274544AD00023E4C /* APIClient.swift in Sources */, @@ -12663,6 +12674,7 @@ C1FFD9FA27ECC7C7008A6848 /* Filter+ChatChannel.swift in Sources */, 4FE6E1AE2BAC7A1B00C80AF1 /* UserListState+Observer.swift in Sources */, 4FE6E1AB2BAC79F400C80AF1 /* MemberListState+Observer.swift in Sources */, + 4FE913882D82C8ED0048AC16 /* BoxedAny.swift in Sources */, AD8FEE592AA8E1A100273F88 /* ChatClient+Environment.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -13711,7 +13723,7 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -13740,7 +13752,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -13770,7 +13782,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Profile; @@ -13864,7 +13876,8 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -13894,7 +13907,8 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -14616,7 +14630,8 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; - SWIFT_VERSION = 5.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Profile; diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec index 925d172201f..ec3b772a031 100644 --- a/StreamChatUI-XCFramework.podspec +++ b/StreamChatUI-XCFramework.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |spec| spec.author = { "getstream.io" => "support@getstream.io" } spec.social_media_url = "https://getstream.io" - spec.swift_version = '5.7' + spec.swift_version = '6.0' spec.platform = :ios, "13.0" spec.requires_arc = true diff --git a/StreamChatUI.podspec b/StreamChatUI.podspec index a05e030d19f..65b7ec7501f 100644 --- a/StreamChatUI.podspec +++ b/StreamChatUI.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |spec| spec.author = { "getstream.io" => "support@getstream.io" } spec.social_media_url = "https://getstream.io" - spec.swift_version = '5.7' + spec.swift_version = '6.0' spec.platform = :ios, "13.0" spec.requires_arc = true diff --git a/TestTools/StreamChatTestTools/Assertions/AssertNetworkRequest.swift b/TestTools/StreamChatTestTools/Assertions/AssertNetworkRequest.swift index 01ba8ec5315..019acaad71d 100644 --- a/TestTools/StreamChatTestTools/Assertions/AssertNetworkRequest.swift +++ b/TestTools/StreamChatTestTools/Assertions/AssertNetworkRequest.swift @@ -7,7 +7,7 @@ import XCTest /// The maximum time `AssertNetworkRequest` waits for the request. When running stress tests, this value /// is much higher because the system might be under very heavy load. -private var assertNetworkRequestTimeout: TimeInterval = TestRunnerEnvironment.isStressTest ? 10 : 1 +private let assertNetworkRequestTimeout: TimeInterval = TestRunnerEnvironment.isStressTest ? 10 : 1 /// Synchronously waits for a network request to be made and asserts its properties. /// diff --git a/TestTools/StreamChatTestTools/Extensions/Unique/TypingEventDTO+Unique.swift b/TestTools/StreamChatTestTools/Extensions/Unique/TypingEventDTO+Unique.swift index eec452d20c3..9cc4c77bf10 100644 --- a/TestTools/StreamChatTestTools/Extensions/Unique/TypingEventDTO+Unique.swift +++ b/TestTools/StreamChatTestTools/Extensions/Unique/TypingEventDTO+Unique.swift @@ -6,7 +6,7 @@ import Foundation @testable import StreamChat extension TypingEventDTO { - static var unique: TypingEventDTO = try! + static let unique: TypingEventDTO = try! .init( from: EventPayload( eventType: .userStartTyping, diff --git a/TestTools/StreamChatTestTools/FakeTimer/FakeTimer.swift b/TestTools/StreamChatTestTools/FakeTimer/FakeTimer.swift index 0dc0190876e..02914dc5aa6 100644 --- a/TestTools/StreamChatTestTools/FakeTimer/FakeTimer.swift +++ b/TestTools/StreamChatTestTools/FakeTimer/FakeTimer.swift @@ -6,28 +6,28 @@ import Foundation @testable import StreamChat class FakeTimer: StreamChat.Timer { - static var mockTimer: TimerControl? - static var mockRepeatingTimer: RepeatingTimerControl? + static let mockTimer = AllocatedUnfairLock(nil) + static let mockRepeatingTimer = AllocatedUnfairLock(nil) static func schedule(timeInterval: TimeInterval, queue: DispatchQueue, onFire: @escaping () -> Void) -> StreamChat.TimerControl { - return mockTimer! + return mockTimer.value! } static func scheduleRepeating(timeInterval: TimeInterval, queue: DispatchQueue, onFire: @escaping () -> Void) -> StreamChat.RepeatingTimerControl { - mockRepeatingTimer! + mockRepeatingTimer.value! } } -class MockTimer: TimerControl { - var cancelCallCount = 0 +class MockTimer: TimerControl, @unchecked Sendable { + @Atomic var cancelCallCount = 0 func cancel() { cancelCallCount += 1 } } -class MockRepeatingTimer: RepeatingTimerControl { - var resumeCallCount = 0 - var suspendCallCount = 0 +class MockRepeatingTimer: RepeatingTimerControl, @unchecked Sendable { + @Atomic var resumeCallCount = 0 + @Atomic var suspendCallCount = 0 func resume() { resumeCallCount += 1 diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/DraftMessage_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/DraftMessage_Mock.swift index bc23edb2345..7b10c8ba989 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/DraftMessage_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/DraftMessage_Mock.swift @@ -35,7 +35,7 @@ public extension DraftMessage { showReplyInChannel: showReplyInChannel, extraData: extraData, currentUser: currentUser, - quotedMessage: { quotedMessage }, + quotedMessage: quotedMessage, mentionedUsers: mentionedUsers, attachments: attachments ) diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift index 2cc0cca1bd8..8fcd294b5c5 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift @@ -6,7 +6,7 @@ import Foundation @testable import StreamChat import XCTest -final class ChatClient_Mock: ChatClient { +final class ChatClient_Mock: ChatClient, @unchecked Sendable { @Atomic var init_config: ChatClientConfig @Atomic var init_environment: Environment @Atomic var init_completion: ((Error?) -> Void)? @@ -129,7 +129,15 @@ extension ChatClient { .init( config: config ?? defaultMockedConfig, environment: .init( - apiClientBuilder: APIClient_Spy.init, + apiClientBuilder: { + APIClient_Spy( + sessionConfiguration: $0, + requestEncoder: $1, + requestDecoder: $2, + attachmentDownloader: $3, + attachmentUploader: $4 + ) + }, webSocketClientBuilder: { WebSocketClient_Mock( sessionConfiguration: $0, @@ -148,13 +156,57 @@ extension ChatClient { internetConnection: { center, _ in InternetConnection_Mock(notificationCenter: center) }, - authenticationRepositoryBuilder: AuthenticationRepository_Mock.init, - syncRepositoryBuilder: SyncRepository_Mock.init, - pollsRepositoryBuilder: PollsRepository_Mock.init, - draftMessagesRepositoryBuilder: DraftMessagesRepository_Mock.init, - channelListUpdaterBuilder: ChannelListUpdater_Spy.init, - messageRepositoryBuilder: MessageRepository_Mock.init, - offlineRequestsRepositoryBuilder: OfflineRequestsRepository_Mock.init + authenticationRepositoryBuilder: { + AuthenticationRepository_Mock( + apiClient: $0, + databaseContainer: $1, + connectionRepository: $2, + tokenExpirationRetryStrategy: $3, + timerType: $4 + ) + }, + syncRepositoryBuilder: { + SyncRepository_Mock( + config: $0, + offlineRequestsRepository: $1, + eventNotificationCenter: $2, + database: $3, + apiClient: $4, + channelListUpdater: $5 + ) + }, + pollsRepositoryBuilder: { + PollsRepository_Mock( + database: $0, + apiClient: $1 + ) + }, + draftMessagesRepositoryBuilder: { + DraftMessagesRepository_Mock( + database: $0, + apiClient: $1 + ) + }, + channelListUpdaterBuilder: { + ChannelListUpdater_Spy( + database: $0, + apiClient: $1 + ) + }, + messageRepositoryBuilder: { + MessageRepository_Mock( + database: $0, + apiClient: $1 + ) + }, + offlineRequestsRepositoryBuilder: { + OfflineRequestsRepository_Mock( + messageRepository: $0, + database: $1, + apiClient: $2, + maxHoursThreshold: $3 + ) + } ) ) } @@ -224,7 +276,15 @@ extension ChatClient { extension ChatClient.Environment { static var mock: ChatClient.Environment { .init( - apiClientBuilder: APIClient_Spy.init, + apiClientBuilder: { + APIClient_Spy( + sessionConfiguration: $0, + requestEncoder: $1, + requestDecoder: $2, + attachmentDownloader: $3, + attachmentUploader: $4 + ) + }, webSocketClientBuilder: { WebSocketClient_Mock( sessionConfiguration: $0, @@ -239,17 +299,69 @@ extension ChatClient.Environment { chatClientConfig: $1 ) }, - requestEncoderBuilder: DefaultRequestEncoder.init, - requestDecoderBuilder: DefaultRequestDecoder.init, - eventDecoderBuilder: EventDecoder.init, - notificationCenterBuilder: EventNotificationCenter.init, - authenticationRepositoryBuilder: AuthenticationRepository_Mock.init, - syncRepositoryBuilder: SyncRepository_Mock.init, - pollsRepositoryBuilder: PollsRepository_Mock.init, - draftMessagesRepositoryBuilder: DraftMessagesRepository_Mock.init, - channelListUpdaterBuilder: ChannelListUpdater_Spy.init, - messageRepositoryBuilder: MessageRepository_Mock.init, - offlineRequestsRepositoryBuilder: OfflineRequestsRepository_Mock.init + requestEncoderBuilder: { + DefaultRequestEncoder(baseURL: $0, apiKey: $1) + }, + requestDecoderBuilder: { + DefaultRequestDecoder() + }, + eventDecoderBuilder: { + EventDecoder() + }, + notificationCenterBuilder: { + EventNotificationCenter(database: $0) + }, + authenticationRepositoryBuilder: { + AuthenticationRepository_Mock( + apiClient: $0, + databaseContainer: $1, + connectionRepository: $2, + tokenExpirationRetryStrategy: $3, + timerType: $4 + ) + }, + syncRepositoryBuilder: { + SyncRepository_Mock( + config: $0, + offlineRequestsRepository: $1, + eventNotificationCenter: $2, + database: $3, + apiClient: $4, + channelListUpdater: $5 + ) + }, + pollsRepositoryBuilder: { + PollsRepository_Mock( + database: $0, + apiClient: $1 + ) + }, + draftMessagesRepositoryBuilder: { + DraftMessagesRepository_Mock( + database: $0, + apiClient: $1 + ) + }, + channelListUpdaterBuilder: { + ChannelListUpdater_Spy( + database: $0, + apiClient: $1 + ) + }, + messageRepositoryBuilder: { + MessageRepository_Mock( + database: $0, + apiClient: $1 + ) + }, + offlineRequestsRepositoryBuilder: { + OfflineRequestsRepository_Mock( + messageRepository: $0, + database: $1, + apiClient: $2, + maxHoursThreshold: $3 + ) + } ) } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/ConnectionRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/ConnectionRepository_Mock.swift index 3c885d7a93c..98a8dce05c2 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/ConnectionRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/ConnectionRepository_Mock.swift @@ -6,7 +6,7 @@ import Foundation /// Mock implementation of `ChatClientUpdater` -final class ConnectionRepository_Mock: ConnectionRepository, Spy { +final class ConnectionRepository_Mock: ConnectionRepository, Spy, @unchecked Sendable { enum Signature { static let initialize = "initialize()" static let connect = "connect(completion:)" @@ -20,18 +20,18 @@ final class ConnectionRepository_Mock: ConnectionRepository, Spy { } let spyState = SpyState() - var connectResult: Result? + @Atomic var connectResult: Result? - var disconnectSource: WebSocketConnectionState.DisconnectionSource? - var disconnectResult: Result? + @Atomic var disconnectSource: WebSocketConnectionState.DisconnectionSource? + @Atomic var disconnectResult: Result? - var updateWebSocketEndpointToken: Token? - var updateWebSocketEndpointUserInfo: UserInfo? - var completeWaitersConnectionId: ConnectionId? - var connectionUpdateState: WebSocketConnectionState? - var simulateExpiredTokenOnConnectionUpdate = false + @Atomic var updateWebSocketEndpointToken: Token? + @Atomic var updateWebSocketEndpointUserInfo: UserInfo? + @Atomic var completeWaitersConnectionId: ConnectionId? + @Atomic var connectionUpdateState: WebSocketConnectionState? + @Atomic var simulateExpiredTokenOnConnectionUpdate = false - var provideConnectionIdResult: Result? + @Atomic var provideConnectionIdResult: Result? convenience init() { self.init(isClientInActiveMode: true, diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/BackgroundEntityDatabaseObserver_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/BackgroundEntityDatabaseObserver_Mock.swift index a8f7b24e3be..ed27a086325 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/BackgroundEntityDatabaseObserver_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/BackgroundEntityDatabaseObserver_Mock.swift @@ -6,9 +6,9 @@ import CoreData @testable import StreamChat import XCTest -final class BackgroundEntityDatabaseObserver_Mock: BackgroundEntityDatabaseObserver { - var synchronizeError: Error? - var startObservingCalled: Bool = false +final class BackgroundEntityDatabaseObserver_Mock: BackgroundEntityDatabaseObserver, @unchecked Sendable { + @Atomic var synchronizeError: Error? + @Atomic var startObservingCalled: Bool = false override func startObserving() throws { if let error = synchronizeError { @@ -19,7 +19,7 @@ final class BackgroundEntityDatabaseObserver_Mock: B } } - var item_mock: Item? + @Atomic var item_mock: Item? override var item: Item? { item_mock ?? super.item } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/BackgroundListDatabaseObserver_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/BackgroundListDatabaseObserver_Mock.swift index 1824fbf15e1..a8295c44970 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/BackgroundListDatabaseObserver_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/BackgroundListDatabaseObserver_Mock.swift @@ -6,8 +6,8 @@ import CoreData @testable import StreamChat import XCTest -final class BackgroundListDatabaseObserver_Mock: BackgroundListDatabaseObserver { - var synchronizeError: Error? +final class BackgroundListDatabaseObserver_Mock: BackgroundListDatabaseObserver, @unchecked Sendable { + @Atomic var synchronizeError: Error? override func startObserving() throws { if let error = synchronizeError { @@ -17,7 +17,7 @@ final class BackgroundListDatabaseObserver_Mock: Bac } } - var items_mock: LazyCachedMapCollection? + @Atomic var items_mock: LazyCachedMapCollection? override var items: LazyCachedMapCollection { items_mock ?? super.items } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChannelListController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChannelListController_Mock.swift index 83cf63fff40..471736a471f 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChannelListController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChannelListController_Mock.swift @@ -6,16 +6,16 @@ import Foundation @testable import StreamChat import XCTest -final class ChannelListController_Mock: ChatChannelListController { +final class ChannelListController_Mock: ChatChannelListController, @unchecked Sendable { @Atomic var synchronize_called = false - var synchronizeCallCount = 0 + @Atomic var synchronizeCallCount = 0 - var channels_simulated: [ChatChannel]? + @Atomic var channels_simulated: [ChatChannel]? override var channels: LazyCachedMapCollection { channels_simulated.map { $0.lazyCachedMap { $0 } } ?? super.channels } - var state_simulated: DataController.State? + @Atomic var state_simulated: DataController.State? override var state: DataController.State { get { state_simulated ?? super.state } set { super.state = newValue } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelController_Mock.swift index 3b77bcf6395..e9e0dc9e65e 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelController_Mock.swift @@ -5,14 +5,14 @@ import Foundation @testable import StreamChat -class ChatChannelController_Mock: ChatChannelController { +class ChatChannelController_Mock: ChatChannelController, @unchecked Sendable { - var mockCid: ChannelId? + @Atomic var mockCid: ChannelId? override var cid: ChannelId? { mockCid ?? super.cid } - var mockFirstUnreadMessageId: MessageId? + @Atomic var mockFirstUnreadMessageId: MessageId? override var firstUnreadMessageId: MessageId? { mockFirstUnreadMessageId ?? super.firstUnreadMessageId } @@ -55,7 +55,7 @@ class ChatChannelController_Mock: ChatChannelController { ) } - var createNewMessageCallCount = 0 + @Atomic var createNewMessageCallCount = 0 override func createNewMessage( messageId: MessageId? = nil, text: String, pinning: MessagePinning? = nil, @@ -72,60 +72,60 @@ class ChatChannelController_Mock: ChatChannelController { createNewMessageCallCount += 1 } - var hasLoadedAllNextMessages_mock: Bool? = true + @Atomic var hasLoadedAllNextMessages_mock: Bool? = true override var hasLoadedAllNextMessages: Bool { hasLoadedAllNextMessages_mock ?? super.hasLoadedAllNextMessages } - var hasLoadedAllPreviousMessages_mock: Bool? = true + @Atomic var hasLoadedAllPreviousMessages_mock: Bool? = true override var hasLoadedAllPreviousMessages: Bool { hasLoadedAllPreviousMessages_mock ?? super.hasLoadedAllPreviousMessages } - var markedAsUnread_mock: Bool? = true + @Atomic var markedAsUnread_mock: Bool? = true override var isMarkedAsUnread: Bool { markedAsUnread_mock ?? super.isMarkedAsUnread } - var channel_mock: ChatChannel? + @Atomic var channel_mock: ChatChannel? override var channel: ChatChannel? { channel_mock ?? super.channel } - var channelQuery_mock: ChannelQuery? + @Atomic var channelQuery_mock: ChannelQuery? override var channelQuery: ChannelQuery { channelQuery_mock ?? super.channelQuery } - var messages_mock: [ChatMessage]? + @Atomic var messages_mock: [ChatMessage]? override var messages: LazyCachedMapCollection { messages_mock.map { $0.lazyCachedMap { $0 } } ?? super.messages } - var markReadCallCount = 0 + @Atomic var markReadCallCount = 0 override func markRead(completion: ((Error?) -> Void)?) { markReadCallCount += 1 } - var state_mock: State? + @Atomic var state_mock: State? override var state: DataController.State { get { state_mock ?? super.state } set { super.state = newValue } } - private(set) var synchronize_completion: ((Error?) -> Void)? + @Atomic private(set) var synchronize_completion: ((Error?) -> Void)? override func synchronize(_ completion: ((Error?) -> Void)? = nil) { synchronize_completion = completion } - var loadFirstPageCallCount = 0 - var loadFirstPage_result: Error? + @Atomic var loadFirstPageCallCount = 0 + @Atomic var loadFirstPage_result: Error? override func loadFirstPage(_ completion: ((Error?) -> Void)? = nil) { loadFirstPageCallCount += 1 completion?(loadFirstPage_result) } - var loadPageAroundMessageIdCallCount = 0 + @Atomic var loadPageAroundMessageIdCallCount = 0 override func loadPageAroundMessageId( _ messageId: MessageId, limit: Int? = nil, @@ -134,9 +134,9 @@ class ChatChannelController_Mock: ChatChannelController { loadPageAroundMessageIdCallCount += 1 } - var updateDraftMessage_callCount = 0 - var updateDraftMessage_completion: ((Result) -> Void)? - var updateDraftMessage_text = "" + @Atomic var updateDraftMessage_callCount = 0 + @Atomic var updateDraftMessage_completion: ((Result) -> Void)? + @Atomic var updateDraftMessage_text = "" override func updateDraftMessage( text: String, @@ -153,16 +153,16 @@ class ChatChannelController_Mock: ChatChannelController { updateDraftMessage_completion = completion } - var deleteDraftMessage_callCount = 0 - var deleteDraftMessage_completion: ((Error?) -> Void)? + @Atomic var deleteDraftMessage_callCount = 0 + @Atomic var deleteDraftMessage_completion: ((Error?) -> Void)? override func deleteDraftMessage(completion: ((Error?) -> Void)? = nil) { deleteDraftMessage_callCount += 1 deleteDraftMessage_completion = completion } - var loadDraftMessage_callCount = 0 - var loadDraftMessage_completion: ((Result) -> Void)? + @Atomic var loadDraftMessage_callCount = 0 + @Atomic var loadDraftMessage_completion: ((Result) -> Void)? override func loadDraftMessage(completion: ((Result) -> Void)? = nil) { loadDraftMessage_callCount += 1 diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift index 22a2ae8e440..8de733dbaf7 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift @@ -5,23 +5,23 @@ import Foundation @testable import StreamChat -class ChatChannelListController_Mock: ChatChannelListController, Spy { +class ChatChannelListController_Mock: ChatChannelListController, Spy, @unchecked Sendable { let spyState = SpyState() - var loadNextChannelsIsCalled = false - var loadNextChannelsCallCount = 0 - var refreshLoadedChannelsResult: Result, any Error>? + @Atomic var loadNextChannelsIsCalled = false + @Atomic var loadNextChannelsCallCount = 0 + @Atomic var refreshLoadedChannelsResult: Result, any Error>? /// Creates a new mock instance of `ChatChannelListController`. static func mock(client: ChatClient? = nil) -> ChatChannelListController_Mock { .init(query: .init(filter: .equal(.memberCount, to: 0)), client: client ?? .mock()) } - var channels_mock: [ChatChannel]? + @Atomic var channels_mock: [ChatChannel]? override var channels: LazyCachedMapCollection { channels_mock.map { $0.lazyCachedMap { $0 } } ?? super.channels } - var state_mock: State? + @Atomic var state_mock: State? override var state: DataController.State { get { state_mock ?? super.state } set { super.state = newValue } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelWatcherListController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelWatcherListController_Mock.swift index 7b2d907cd39..2e9e5a87876 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelWatcherListController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelWatcherListController_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// A mock for `ChatChannelWatcherListController`. -final class ChatChannelWatcherListController_Mock: ChatChannelWatcherListController { +final class ChatChannelWatcherListController_Mock: ChatChannelWatcherListController, @unchecked Sendable { @Atomic var watchers_simulated: [ChatUser]? override var watchers: LazyCachedMapCollection { watchers_simulated.map { $0.lazyCachedMap { $0 } } ?? super.watchers diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatMessageController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatMessageController_Mock.swift index 7603a7ff9fd..7c9bf3c8064 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatMessageController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatMessageController_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamChat -class ChatMessageController_Mock: ChatMessageController { +class ChatMessageController_Mock: ChatMessageController, @unchecked Sendable { /// Creates a new mock instance of `ChatMessageController`. static func mock( currentUserId: UserId = "ID", @@ -23,23 +23,23 @@ class ChatMessageController_Mock: ChatMessageController { return .init(client: chatClient, cid: channelId!, messageId: messageId, replyPaginationHandler: MessagesPaginationStateHandler_Mock()) } - var message_mock: ChatMessage? + @Atomic var message_mock: ChatMessage? override var message: ChatMessage? { message_mock ?? super.message } - var replies_mock: [ChatMessage]? + @Atomic var replies_mock: [ChatMessage]? override var replies: LazyCachedMapCollection { replies_mock.map { $0.lazyCachedMap { $0 } } ?? super.replies } - var state_mock: State? + @Atomic var state_mock: State? override var state: DataController.State { get { state_mock ?? super.state } set { super.state = newValue } } - var startObserversIfNeeded_mock: (() -> Void)? + @Atomic var startObserversIfNeeded_mock: (() -> Void)? override func startObserversIfNeeded() { if let mock = startObserversIfNeeded_mock { mock() @@ -49,16 +49,16 @@ class ChatMessageController_Mock: ChatMessageController { super.startObserversIfNeeded() } - var synchronize_callCount = 0 - var synchronize_completion: ((Error?) -> Void)? + @Atomic var synchronize_callCount = 0 + @Atomic var synchronize_completion: ((Error?) -> Void)? override func synchronize(_ completion: ((Error?) -> Void)? = nil) { synchronize_callCount += 1 synchronize_completion = completion } - var loadPageAroundReplyId_callCount = 0 - var loadPageAroundReplyId_completion: ((Error?) -> Void)? + @Atomic var loadPageAroundReplyId_callCount = 0 + @Atomic var loadPageAroundReplyId_completion: ((Error?) -> Void)? override func loadPageAroundReplyId( _ replyId: MessageId, limit: Int? = nil, diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatMessageSearchController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatMessageSearchController_Mock.swift index 3e66ca12ed7..8a9483f7595 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatMessageSearchController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatMessageSearchController_Mock.swift @@ -5,17 +5,17 @@ import Foundation @testable import StreamChat -class ChatMessageSearchController_Mock: ChatMessageSearchController { +class ChatMessageSearchController_Mock: ChatMessageSearchController, @unchecked Sendable { static func mock(client: ChatClient? = nil) -> ChatMessageSearchController_Mock { .init(client: client ?? .mock()) } - var messages_mock: LazyCachedMapCollection? + @Atomic var messages_mock: LazyCachedMapCollection? override var messages: LazyCachedMapCollection { messages_mock ?? super.messages } - var state_mock: DataController.State? + @Atomic var state_mock: DataController.State? override var state: DataController.State { get { state_mock ?? super.state @@ -25,13 +25,13 @@ class ChatMessageSearchController_Mock: ChatMessageSearchController { } } - var loadNextMessagesCallCount = 0 + @Atomic var loadNextMessagesCallCount = 0 override func loadNextMessages(limit: Int = 25, completion: ((Error?) -> Void)? = nil) { loadNextMessagesCallCount += 1 completion?(nil) } - var searchCallCount = 0 + @Atomic var searchCallCount = 0 override func search(query: MessageSearchQuery, completion: ((Error?) -> Void)? = nil) { searchCallCount += 1 completion?(nil) diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatThreadListController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatThreadListController_Mock.swift index 6b3f070bc7f..a6cff444117 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatThreadListController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatThreadListController_Mock.swift @@ -5,24 +5,24 @@ import Foundation @testable import StreamChat -class ChatThreadListController_Mock: ChatThreadListController { +class ChatThreadListController_Mock: ChatThreadListController, @unchecked Sendable { static func mock(query: ThreadListQuery, client: ChatClient? = nil) -> ChatThreadListController_Mock { .init(query: query, client: client ?? .mock()) } - var threads_mock: [ChatThread]? + @Atomic var threads_mock: [ChatThread]? override var threads: LazyCachedMapCollection { threads_mock.map { $0.lazyCachedMap { $0 } } ?? super.threads } - var state_mock: State? + @Atomic var state_mock: State? override var state: DataController.State { get { state_mock ?? super.state } set { super.state = newValue } } - var synchronize_completion: (((any Error)?) -> Void)? - var synchronize_callCount = 0 + @Atomic var synchronize_completion: (((any Error)?) -> Void)? + @Atomic var synchronize_callCount = 0 override func synchronize(_ completion: (((any Error)?) -> Void)? = nil) { synchronize_callCount += 1 synchronize_completion = completion diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatUserSearchController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatUserSearchController_Mock.swift index dcd5a3d5913..b1dca450658 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatUserSearchController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatUserSearchController_Mock.swift @@ -5,15 +5,15 @@ import Foundation @testable import StreamChat -class ChatUserSearchController_Mock: ChatUserSearchController { +class ChatUserSearchController_Mock: ChatUserSearchController, @unchecked Sendable { - var searchCallCount = 0 + @Atomic var searchCallCount = 0 static func mock(client: ChatClient? = nil) -> ChatUserSearchController_Mock { .init(client: client ?? .mock()) } - var users_mock: [ChatUser]? + @Atomic var users_mock: [ChatUser]? override var userArray: [ChatUser] { users_mock ?? super.userArray } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/CurrentChatUserController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/CurrentChatUserController_Mock.swift index c7e43cf779c..61fc408f75c 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/CurrentChatUserController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/CurrentChatUserController_Mock.swift @@ -5,12 +5,12 @@ import Foundation @testable import StreamChat -class CurrentChatUserController_Mock: CurrentChatUserController { +class CurrentChatUserController_Mock: CurrentChatUserController, @unchecked Sendable { static func mock(client: ChatClient? = nil) -> CurrentChatUserController_Mock { .init(client: client ?? .mock()) } - var currentUser_mock: CurrentChatUser? + @Atomic var currentUser_mock: CurrentChatUser? override var currentUser: CurrentChatUser? { currentUser_mock } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/CurrentUserController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/CurrentUserController_Mock.swift index 29d4dc75607..08a9affe3d8 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/CurrentUserController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/CurrentUserController_Mock.swift @@ -5,13 +5,13 @@ import Foundation @testable import StreamChat -final class CurrentUserController_Mock: CurrentChatUserController { - var currentUser_simulated: CurrentChatUser? +final class CurrentUserController_Mock: CurrentChatUserController, @unchecked Sendable { + @Atomic var currentUser_simulated: CurrentChatUser? override var currentUser: CurrentChatUser? { currentUser_simulated ?? super.currentUser } - var unreadCount_simulated: UnreadCount? + @Atomic var unreadCount_simulated: UnreadCount? override var unreadCount: UnreadCount { unreadCount_simulated ?? super.unreadCount } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/PollController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/PollController_Mock.swift index b2bc05c2510..f311409b7ee 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/PollController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/PollController_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamChat -final class PollController_Mock: PollController { +final class PollController_Mock: PollController, @unchecked Sendable { @Atomic var synchronize_called = false @Atomic var synchronize_completion_result: Result? @Atomic var castPollVote_called = false @@ -17,17 +17,17 @@ final class PollController_Mock: PollController { @Atomic var suggestPollOption_called = false @Atomic var suggestPollOption_completion_result: Result? - var poll_simulated: Poll? + @Atomic var poll_simulated: Poll? override var poll: Poll? { poll_simulated } - var ownVotes_simulated: LazyCachedMapCollection = .init([]) + @Atomic var ownVotes_simulated: LazyCachedMapCollection = .init([]) override var ownVotes: LazyCachedMapCollection { ownVotes_simulated } - var state_simulated: DataController.State? + @Atomic var state_simulated: DataController.State? override var state: DataController.State { get { state_simulated ?? super.state } set { super.state = newValue } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/PollVoteListController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/PollVoteListController_Mock.swift index 69e67689596..7753ecff6f7 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/PollVoteListController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/PollVoteListController_Mock.swift @@ -5,18 +5,18 @@ import Foundation @testable import StreamChat -final class PollVoteListController_Mock: PollVoteListController { +final class PollVoteListController_Mock: PollVoteListController, @unchecked Sendable { @Atomic var synchronize_called = false @Atomic var synchronize_completion_result: Result? @Atomic var loadMoreVotes_limit: Int? @Atomic var loadMoreVotes_completion_result: Result? - var votes_simulated: LazyCachedMapCollection = .init([]) + @Atomic var votes_simulated: LazyCachedMapCollection = .init([]) override var votes: LazyCachedMapCollection { votes_simulated } - var state_simulated: DataController.State? + @Atomic var state_simulated: DataController.State? override var state: DataController.State { get { state_simulated ?? super.state } set { super.state = newValue } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/MemberListController/MemberListController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/MemberListController/MemberListController_Mock.swift index 67c60a16cb1..3a3b157b0dc 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/MemberListController/MemberListController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/MemberListController/MemberListController_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// A mock for `ChatChannelMemberListController`. -final class ChatChannelMemberListController_Mock: ChatChannelMemberListController { +final class ChatChannelMemberListController_Mock: ChatChannelMemberListController, @unchecked Sendable { @Atomic var members_simulated: [ChatChannelMember]? override var members: LazyCachedMapCollection { members_simulated.map { $0.lazyCachedMap { $0 } } ?? super.members diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/InternetConnection_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/InternetConnection_Mock.swift index 4926a762454..a30eb8165ed 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/InternetConnection_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/InternetConnection_Mock.swift @@ -6,9 +6,9 @@ import Foundation @testable import StreamChat /// Mock implementation of `InternetConnection` -final class InternetConnection_Mock: InternetConnection { - private(set) var monitorMock: InternetConnectionMonitor_Mock! - private(set) var init_notificationCenter: NotificationCenter! +final class InternetConnection_Mock: InternetConnection, @unchecked Sendable { + @Atomic private(set) var monitorMock: InternetConnectionMonitor_Mock! + @Atomic private(set) var init_notificationCenter: NotificationCenter! init( monitor: InternetConnectionMonitor_Mock = .init(), @@ -21,7 +21,7 @@ final class InternetConnection_Mock: InternetConnection { } /// Mock implementation of `InternetConnectionMonitor` -final class InternetConnectionMonitor_Mock: InternetConnectionMonitor { +final class InternetConnectionMonitor_Mock: InternetConnectionMonitor, @unchecked Sendable { weak var delegate: InternetConnectionDelegate? var status: InternetConnection.Status = .unknown { diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/RequestRecorderURLProtocol_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/RequestRecorderURLProtocol_Mock.swift index 1d5e4cf3a21..e3aac81256e 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/RequestRecorderURLProtocol_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/RequestRecorderURLProtocol_Mock.swift @@ -11,9 +11,9 @@ import XCTest final class RequestRecorderURLProtocol_Mock: URLProtocol { /// If set, records only requests with `testSessionHeaderKey` header value set to this value. If `nil`, /// no requests are recorded. - @Atomic static var currentSessionId: String? - @Atomic private static var latestRequestExpectation: XCTestExpectation? - @Atomic private static var latestRequest: URLRequest? + static let currentSessionId = AllocatedUnfairLock(nil) + private static let latestRequestExpectation = AllocatedUnfairLock(nil) + private static let latestRequest = AllocatedUnfairLock(nil) static let testSessionHeaderKey = "RequestRecorderURLProtocolMock_test_session_id" @@ -22,7 +22,7 @@ final class RequestRecorderURLProtocol_Mock: URLProtocol { static func startTestSession(with configuration: inout URLSessionConfiguration) { reset() let newSessionId = UUID().uuidString - currentSessionId = newSessionId + currentSessionId.value = newSessionId configuration.protocolClasses?.insert(Self.self, at: 0) var existingHeaders = configuration.httpAdditionalHeaders ?? [:] @@ -38,23 +38,23 @@ final class RequestRecorderURLProtocol_Mock: URLProtocol { /// - Parameter timeout: Specifies the time the function waits for a new request to be made. static func waitForRequest(timeout: TimeInterval) -> URLRequest? { defer { reset() } - guard latestRequest == nil else { return latestRequest } + guard latestRequest.value == nil else { return latestRequest.value } - latestRequestExpectation = .init(description: "Wait for incoming request.") - _ = XCTWaiter.wait(for: [latestRequestExpectation!], timeout: timeout) - return latestRequest + latestRequestExpectation.value = .init(description: "Wait for incoming request.") + _ = XCTWaiter.wait(for: [latestRequestExpectation.value!], timeout: timeout) + return latestRequest.value } /// Cleans up existing waiters and recorded requests. We have to explictly reset the state because URLProtocols /// work with static variables. static func reset() { - currentSessionId = nil - latestRequest = nil - latestRequestExpectation = nil + currentSessionId.value = nil + latestRequest.value = nil + latestRequestExpectation.value = nil } override class func canInit(with request: URLRequest) -> Bool { - guard let sessionId = currentSessionId else { return false } + guard let sessionId = currentSessionId.value else { return false } if sessionId == request.value(forHTTPHeaderField: testSessionHeaderKey) { record(request: request) @@ -68,12 +68,12 @@ final class RequestRecorderURLProtocol_Mock: URLProtocol { } private static func record(request: URLRequest) { - guard latestRequest == nil else { + guard latestRequest.value == nil else { log.info("Request for \(String(describing: currentSessionId)) already recoded. Skipping.") return } - latestRequest = request - latestRequestExpectation?.fulfill() + latestRequest.value = request + latestRequestExpectation.value?.fulfill() } // MARK: Instance methods diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/URLProtocol_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/URLProtocol_Mock.swift index f7804941825..6d17f79e06b 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/URLProtocol_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/MockNetwork/URLProtocol_Mock.swift @@ -16,7 +16,7 @@ final class URLProtocol_Mock: URLProtocol { static func startTestSession(with configuration: inout URLSessionConfiguration) { reset() let newSessionId = UUID().uuidString - currentSessionId = newSessionId + currentSessionId.value = newSessionId // URLProtocol_Mock always has to be first, but not if the RequestRecorderURLProtocol_Mock is presented if let recorderProtocolIdx = configuration.protocolClasses? @@ -31,27 +31,27 @@ final class URLProtocol_Mock: URLProtocol { configuration.httpAdditionalHeaders = existingHeaders } - @Atomic private static var responses: [PathAndMethod: MockResponse] = [:] + private static let responses = AllocatedUnfairLock<[PathAndMethod: MockResponse]>([:]) /// If set, the mock protocol responds to requests with `testSessionHeaderKey` header value set to this value. If `nil`, /// all requests are ignored. - @Atomic static var currentSessionId: String? + static let currentSessionId = AllocatedUnfairLock(nil) /// Cleans up all existing mock responses and current test session id. static func reset() { - Self.currentSessionId = nil - Self.responses.removeAll() + Self.currentSessionId.value = nil + Self.responses.withLock { $0.removeAll() } } override class func canInit(with request: URLRequest) -> Bool { guard - request.value(forHTTPHeaderField: testSessionHeaderKey) == currentSessionId, + request.value(forHTTPHeaderField: testSessionHeaderKey) == currentSessionId.value, let url = request.url, let method = request.httpMethod else { return false } let key = PathAndMethod(url: url, method: method) - return responses.keys.contains(key) + return responses.withLock { $0.keys.contains(key) } } override class func canonicalRequest(for request: URLRequest) -> URLRequest { @@ -65,7 +65,7 @@ final class URLProtocol_Mock: URLProtocol { guard let url = request.url, let method = request.httpMethod, - let mockResponse = Self.responses[.init(url: url, method: method)] + let mockResponse = Self.responses.value[PathAndMethod(url: url, method: method)] else { fatalError("This should never happen. Check if the implementation of the `canInit` method is correct.") } @@ -90,7 +90,7 @@ final class URLProtocol_Mock: URLProtocol { client?.urlProtocolDidFinishLoading(self) // Clean up - Self._responses.mutate { + Self.responses.withLock { $0.removeValue(forKey: .init(url: url, method: method)) } } @@ -109,7 +109,7 @@ extension URLProtocol_Mock { /// - response: The JSON body of the response. static func mockResponse(request: URLRequest, statusCode: Int = 200, responseBody: Data = Data([])) { let key = PathAndMethod(url: request.url!, method: request.httpMethod!) - Self._responses.mutate { + Self.responses.withLock { $0[key] = MockResponse(result: .success(responseBody), responseCode: statusCode) } } @@ -122,7 +122,7 @@ extension URLProtocol_Mock { /// - error: The error object used for the response. static func mockResponse(request: URLRequest, statusCode: Int = 400, error: Error) { let key = PathAndMethod(url: request.url!, method: request.httpMethod!) - Self._responses.mutate { + Self.responses.withLock { $0[key] = MockResponse(result: .failure(error), responseCode: statusCode) } } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift index 54183c8f911..b0a869d5451 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamChat -class AuthenticationRepository_Mock: AuthenticationRepository, Spy { +class AuthenticationRepository_Mock: AuthenticationRepository, Spy, @unchecked Sendable { enum Signature { static let connectTokenProvider = "connectUser(userInfo:tokenProvider:completion:)" static let connectGuest = "connectGuestUser(userInfo:completion:)" @@ -20,14 +20,14 @@ class AuthenticationRepository_Mock: AuthenticationRepository, Spy { } let spyState = SpyState() - var mockedToken: Token? - var mockedCurrentUserId: UserId? + @Atomic var mockedToken: Token? + @Atomic var mockedCurrentUserId: UserId? - var connectUserResult: Result? - var connectGuestResult: Result? - var connectAnonResult: Result? - var refreshTokenResult: Result? - var completeWaitersToken: Token? + @Atomic var connectUserResult: Result? + @Atomic var connectGuestResult: Result? + @Atomic var connectAnonResult: Result? + @Atomic var refreshTokenResult: Result? + @Atomic var completeWaitersToken: Token? override var currentUserId: UserId? { return mockedCurrentUserId @@ -95,7 +95,7 @@ class AuthenticationRepository_Mock: AuthenticationRepository, Spy { record() } - var resetCallCount: Int = 0 + @Atomic var resetCallCount: Int = 0 override func reset() { resetCallCount += 1 } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift index f2838726c64..40659735476 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/ChannelRepository_Mock.swift @@ -9,20 +9,20 @@ @testable import StreamChat import Foundation -class ChannelRepository_Mock: ChannelRepository, Spy { +class ChannelRepository_Mock: ChannelRepository, Spy, @unchecked Sendable { let spyState = SpyState() - var getChannel_store: Bool? - var getChannel_result: Result? + @Atomic var getChannel_store: Bool? + @Atomic var getChannel_result: Result? - var markReadCid: ChannelId? - var markReadUserId: UserId? - var markReadResult: Result? + @Atomic var markReadCid: ChannelId? + @Atomic var markReadUserId: UserId? + @Atomic var markReadResult: Result? - var markUnreadCid: ChannelId? - var markUnreadUserId: UserId? - var markUnreadMessageId: UserId? - var markUnreadLastReadMessageId: UserId? - var markUnreadResult: Result? + @Atomic var markUnreadCid: ChannelId? + @Atomic var markUnreadUserId: UserId? + @Atomic var markUnreadMessageId: UserId? + @Atomic var markUnreadLastReadMessageId: UserId? + @Atomic var markUnreadResult: Result? override func getChannel(for query: ChannelQuery, store: Bool, completion: @escaping (Result) -> Void) { record() diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/DraftMessagesRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/DraftMessagesRepository_Mock.swift index ef00a1b8693..fe3dc623c34 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/DraftMessagesRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/DraftMessagesRepository_Mock.swift @@ -5,12 +5,12 @@ @testable import StreamChat import XCTest -final class DraftMessagesRepository_Mock: DraftMessagesRepository { +final class DraftMessagesRepository_Mock: DraftMessagesRepository, @unchecked Sendable { // MARK: - Load Drafts - var loadDrafts_callCount = 0 - var loadDrafts_calledWith: DraftListQuery? - var loadDrafts_completion: ((Result) -> Void)? + @Atomic var loadDrafts_callCount = 0 + @Atomic var loadDrafts_calledWith: DraftListQuery? + @Atomic var loadDrafts_completion: ((Result) -> Void)? override func loadDrafts( query: DraftListQuery, @@ -23,8 +23,8 @@ final class DraftMessagesRepository_Mock: DraftMessagesRepository { // MARK: - Update Draft - var updateDraft_callCount = 0 - var updateDraft_calledWith: ( + @Atomic var updateDraft_callCount = 0 + @Atomic var updateDraft_calledWith: ( cid: ChannelId, threadId: MessageId?, text: String, @@ -37,7 +37,7 @@ final class DraftMessagesRepository_Mock: DraftMessagesRepository { quotedMessageId: MessageId?, extraData: [String: RawJSON] )? - var updateDraft_completion: ((Result) -> Void)? + @Atomic var updateDraft_completion: ((Result) -> Void)? override func updateDraft( for cid: ChannelId, @@ -72,9 +72,9 @@ final class DraftMessagesRepository_Mock: DraftMessagesRepository { // MARK: - Get Draft - var getDraft_callCount = 0 - var getDraft_calledWith: (cid: ChannelId, threadId: MessageId?)? - var getDraft_completion: ((Result) -> Void)? + @Atomic var getDraft_callCount = 0 + @Atomic var getDraft_calledWith: (cid: ChannelId, threadId: MessageId?)? + @Atomic var getDraft_completion: ((Result) -> Void)? override func getDraft( for cid: ChannelId, @@ -88,9 +88,9 @@ final class DraftMessagesRepository_Mock: DraftMessagesRepository { // MARK: - Delete Draft - var deleteDraft_callCount = 0 - var deleteDraft_calledWith: (cid: ChannelId, threadId: MessageId?)? - var deleteDraft_completion: ((Error?) -> Void)? + @Atomic var deleteDraft_callCount = 0 + @Atomic var deleteDraft_calledWith: (cid: ChannelId, threadId: MessageId?)? + @Atomic var deleteDraft_completion: ((Error?) -> Void)? override func deleteDraft( for cid: ChannelId, diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/MessageRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/MessageRepository_Mock.swift index b6324861163..2378a88aa5a 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/MessageRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/MessageRepository_Mock.swift @@ -5,19 +5,19 @@ import Foundation @testable import StreamChat -final class MessageRepository_Mock: MessageRepository, Spy { +final class MessageRepository_Mock: MessageRepository, Spy, @unchecked Sendable { let spyState = SpyState() var sendMessageIds: [MessageId] { Array(sendMessageCalls.keys) } - var sendMessageResult: Result? + @Atomic var sendMessageResult: Result? @Atomic var sendMessageCalls: [MessageId: (Result) -> Void] = [:] - var getMessageResult: Result? - var getMessage_store: Bool? - var saveSuccessfullyDeletedMessageError: Error? - var updatedMessageLocalState: LocalMessageState? - var updateMessageResult: Result? + @Atomic var getMessageResult: Result? + @Atomic var getMessage_store: Bool? + @Atomic var saveSuccessfullyDeletedMessageError: Error? + @Atomic var updatedMessageLocalState: LocalMessageState? + @Atomic var updateMessageResult: Result? override func sendMessage( with messageId: MessageId, diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/OfflineRequestsRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/OfflineRequestsRepository_Mock.swift index 900e9453cf3..2d9d72617fb 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/OfflineRequestsRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/OfflineRequestsRepository_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamChat -final class OfflineRequestsRepository_Mock: OfflineRequestsRepository, Spy { +final class OfflineRequestsRepository_Mock: OfflineRequestsRepository, Spy, @unchecked Sendable { let spyState = SpyState() convenience init() { diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/PollsRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/PollsRepository_Mock.swift index 22ce49b941e..73c1791207f 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/PollsRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/PollsRepository_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamChat -final class PollsRepository_Mock: PollsRepository, Spy { +final class PollsRepository_Mock: PollsRepository, Spy, @unchecked Sendable { @Atomic var getQueryPollVotes_completion: ((Result) -> Void)? @Atomic var castPollVote_completion: ((Error?) -> Void)? @Atomic var removePollVote_completion: ((Error?) -> Void)? @@ -13,8 +13,7 @@ final class PollsRepository_Mock: PollsRepository, Spy { @Atomic var suggestPollOption_completion: ((Error?) -> Void)? @Atomic var deletePoll_completion: ((Error?) -> Void)? - var recordedFunctions: [String] = [] - var spyState: SpyState = .init() + let spyState: SpyState = .init() override func queryPollVotes( query: PollVoteListQuery, diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/SyncRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/SyncRepository_Mock.swift index 3e054623255..ba40671daeb 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/SyncRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/SyncRepository_Mock.swift @@ -5,13 +5,13 @@ import Foundation @testable import StreamChat -final class SyncRepository_Mock: SyncRepository, Spy { +final class SyncRepository_Mock: SyncRepository, Spy, @unchecked Sendable { enum Signature { static let cancelRecoveryFlow = "cancelRecoveryFlow()" } let spyState = SpyState() - var syncMissingEventsResult: Result<[ChannelId], SyncError>? + @Atomic var syncMissingEventsResult: Result<[ChannelId], SyncError>? convenience init() { let apiClient = APIClient_Spy() diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/ChannelList_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/ChannelList_Mock.swift index 092c7fabb0a..204bbd43116 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/ChannelList_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/ChannelList_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamChat -public class ChannelList_Mock: ChannelList { +public class ChannelList_Mock: ChannelList, @unchecked Sendable { public static func mock( query: ChannelListQuery? = nil, @@ -19,7 +19,7 @@ public class ChannelList_Mock: ChannelList { override init( query: ChannelListQuery, - dynamicFilter: ((ChatChannel) -> Bool)? = nil, + dynamicFilter: (@Sendable(ChatChannel) -> Bool)? = nil, client: ChatClient, environment: ChannelList.Environment = .init() ) { @@ -35,7 +35,7 @@ public class ChannelList_Mock: ChannelList { state.channels = StreamCollection(channels) } - public var loadNextChannelsIsCalled = false + @Atomic public var loadNextChannelsIsCalled = false public override func loadMoreChannels(limit: Int? = nil) async throws -> [ChatChannel] { loadNextChannelsIsCalled = true return await MainActor.run { diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift index 4eb034f924a..9b68a4132ab 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamChat -public class Chat_Mock: Chat, Spy { +public class Chat_Mock: Chat, Spy, @unchecked Sendable { public let spyState = SpyState() static let cid = try! ChannelId(cid: "mock:channel") @@ -48,7 +48,7 @@ public class Chat_Mock: Chat, Spy { ) } - var createNewMessageCallCount = 0 + @Atomic var createNewMessageCallCount = 0 public override func sendMessage( with text: String, attachments: [AnyAttachmentPayload] = [], @@ -66,7 +66,7 @@ public class Chat_Mock: Chat, Spy { return ChatMessage.mock() } - public var loadPageAroundMessageIdCallCount = 0 + @Atomic public var loadPageAroundMessageIdCallCount = 0 public override func loadMessages(around messageId: MessageId, limit: Int? = nil) async throws { loadPageAroundMessageIdCallCount += 1 } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/MessageSearch_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/MessageSearch_Mock.swift index 12f9ce93c6d..34c3657fe80 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/MessageSearch_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/MessageSearch_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamChat -public class MessageSearch_Mock: MessageSearch { +public class MessageSearch_Mock: MessageSearch, @unchecked Sendable { public static func mock(client: ChatClient? = nil) -> MessageSearch_Mock { .init(client: client ?? .mock(bundle: Bundle(for: Self.self))) } @@ -20,13 +20,13 @@ public class MessageSearch_Mock: MessageSearch { messages_mock ?? super.state.messages } - var loadNextMessagesCallCount = 0 + @Atomic var loadNextMessagesCallCount = 0 public override func loadMoreMessages(limit: Int? = nil) async throws -> [ChatMessage] { loadNextMessagesCallCount += 1 return await Array(state.messages) } - var searchCallCount = 0 + @Atomic var searchCallCount = 0 public override func search(query: MessageSearchQuery) async throws -> [ChatMessage] { searchCallCount += 1 return await MainActor.run { diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/UserSearch_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/UserSearch_Mock.swift index 7717582dfa3..1cf51f8f01b 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/UserSearch_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/UserSearch_Mock.swift @@ -5,9 +5,9 @@ import Foundation @testable import StreamChat -public class UserSearch_Mock: UserSearch { +public class UserSearch_Mock: UserSearch, @unchecked Sendable { - var searchCallCount = 0 + @Atomic var searchCallCount = 0 public static func mock(client: ChatClient? = nil) -> UserSearch_Mock { .init(client: client ?? .mock(bundle: Bundle(for: Self.self))) diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/EventBatcher_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/EventBatcher_Mock.swift index 6de97217c74..5e3369d4c76 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/EventBatcher_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/EventBatcher_Mock.swift @@ -5,15 +5,15 @@ import Foundation @testable import StreamChat -final class EventBatcher_Mock: EventBatcher { - var currentBatch: [Event] = [] +final class EventBatcher_Mock: EventBatcher, @unchecked Sendable { + @Atomic var currentBatch: [Event] = [] - let handler: (_ batch: [Event], _ completion: @escaping () -> Void) -> Void + let handler: (_ batch: [Event], _ completion: @escaping @Sendable() -> Void) -> Void init( period: TimeInterval = 0, timerType: StreamChat.Timer.Type = DefaultTimer.self, - handler: @escaping (_ batch: [Event], _ completion: @escaping () -> Void) -> Void + handler: @escaping (_ batch: [Event], _ completion: @escaping @Sendable () -> Void) -> Void ) { self.handler = handler } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/MessagesPaginationStateHandler_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/MessagesPaginationStateHandler_Mock.swift index 63687b18906..36d7ecfdcbd 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/MessagesPaginationStateHandler_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/MessagesPaginationStateHandler_Mock.swift @@ -9,15 +9,15 @@ import Foundation @testable import StreamChat -final class MessagesPaginationStateHandler_Mock: MessagesPaginationStateHandling { +final class MessagesPaginationStateHandler_Mock: MessagesPaginationStateHandling, @unchecked Sendable { - var mockState: MessagesPaginationState = .initial + @Atomic var mockState: MessagesPaginationState = .initial - var beginCallCount = 0 - var beginCalledWith: MessagesPagination? + @Atomic var beginCallCount = 0 + @Atomic var beginCalledWith: MessagesPagination? - var endCallCount = 0 - var endCalledWith: (MessagesPagination, Result<[MessagePayload], Error>)? + @Atomic var endCallCount = 0 + @Atomic var endCalledWith: (MessagesPagination, Result<[MessagePayload], Error>)? var state: MessagesPaginationState { mockState diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/ScheduledStreamTimer_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/ScheduledStreamTimer_Mock.swift index 3719616b7bf..eb7936fbe4c 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/ScheduledStreamTimer_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Utils/ScheduledStreamTimer_Mock.swift @@ -6,11 +6,11 @@ import Foundation import StreamChat public final class ScheduledStreamTimer_Mock: StreamTimer { - public var stopCallCount: Int = 0 - public var startCallCount: Int = 0 + @Atomic public var stopCallCount: Int = 0 + @Atomic public var startCallCount: Int = 0 public var isRunning: Bool = false - public var onChange: (() -> Void)? + public var onChange: (@Sendable() -> Void)? public init() {} diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAVPlayer.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAVPlayer.swift index a4fccce1a77..2298decb862 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAVPlayer.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAVPlayer.swift @@ -3,7 +3,7 @@ // import AVFoundation -import StreamChat +@testable import StreamChat public class MockAVPlayer: AVPlayer { public var playWasCalled = false @@ -44,8 +44,10 @@ public class MockAVPlayer: AVPlayer { override public func replaceCurrentItem( with item: AVPlayerItem? ) { - replaceCurrentItemWasCalled = true - replaceCurrentItemWasCalledWithItem = item + MainActor.ensureIsolated { + replaceCurrentItemWasCalled = true + replaceCurrentItemWasCalledWithItem = item + } super.replaceCurrentItem(with: item) } @@ -53,16 +55,22 @@ public class MockAVPlayer: AVPlayer { to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime, - completionHandler: @escaping (Bool) -> Void + completionHandler: @escaping @Sendable(Bool) -> Void ) { - seekWasCalledWithTime = time - seekWasCalledWithToleranceBefore = toleranceBefore - seekWasCalledWithToleranceAfter = toleranceAfter + MainActor.ensureIsolated { + seekWasCalledWithTime = time + seekWasCalledWithToleranceBefore = toleranceBefore + seekWasCalledWithToleranceAfter = toleranceAfter + } super.seek( to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter, - completionHandler: { _ in completionHandler(!self.holdSeekCompletion) } + completionHandler: { _ in + MainActor.ensureIsolated { + completionHandler(!self.holdSeekCompletion) + } + } ) } } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAssetPropertyLoader.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAssetPropertyLoader.swift index d3658b5cfee..2af9c81d5d7 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAssetPropertyLoader.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAssetPropertyLoader.swift @@ -5,7 +5,7 @@ import AVFoundation @testable import StreamChat -open class MockAssetPropertyLoader: AssetPropertyLoading { +open class MockAssetPropertyLoader: AssetPropertyLoading, @unchecked Sendable { open var loadPropertiesWasCalledWithProperties: [AssetProperty]? open var loadPropertiesWasCalledWithAsset: AVAsset? open var loadPropertiesResult: Result? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAudioAnalyser.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAudioAnalyser.swift index 8bcc3202416..f7c8fea7b8b 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAudioAnalyser.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAudioAnalyser.swift @@ -6,9 +6,25 @@ import Foundation @testable import StreamChat public final class MockAudioAnalyser: AudioAnalysing { - public private(set) var analyseWasCalledWithAudioAnalysisContext: AudioAnalysisContext? - public private(set) var analyseWasCalledWithTargetSamples: Int? - public var analyseResult: Result<[Float], Error> = .success([]) + private let queue = DispatchQueue(label: "io.getstream.mock-audio-analyzer", target: .global()) + + public private(set) var analyseWasCalledWithAudioAnalysisContext: AudioAnalysisContext? { + get { queue.sync { _analyseWasCalledWithAudioAnalysisContext } } + set { queue.sync { _analyseWasCalledWithAudioAnalysisContext = newValue } } + } + nonisolated(unsafe) private var _analyseWasCalledWithAudioAnalysisContext: AudioAnalysisContext? + + public private(set) var analyseWasCalledWithTargetSamples: Int? { + get { queue.sync { _analyseWasCalledWithTargetSamples } } + set { queue.sync { _analyseWasCalledWithTargetSamples = newValue } } + } + nonisolated(unsafe) private var _analyseWasCalledWithTargetSamples: Int? + + public var analyseResult: Result<[Float], Error> { + get { queue.sync { _analyseResult } } + set { queue.sync { _analyseResult = newValue } } + } + nonisolated(unsafe) private var _analyseResult: Result<[Float], Error> = .success([]) public init() {} diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAudioPlayer.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAudioPlayer.swift index 780d1328710..e6de235e866 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAudioPlayer.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/VoiceRecording/MockAudioPlayer.swift @@ -5,7 +5,7 @@ import Foundation import StreamChat -public class MockAudioPlayer: AudioPlaying { +public class MockAudioPlayer: AudioPlaying, @unchecked Sendable { public private(set) var subscribeWasCalledWithSubscriber: AudioPlayingDelegate? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/BackgroundTaskScheduler_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/BackgroundTaskScheduler_Mock.swift index e9c0d7d46d6..e79cc3f58c2 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/BackgroundTaskScheduler_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/BackgroundTaskScheduler_Mock.swift @@ -6,31 +6,31 @@ import Foundation @testable import StreamChat /// Mock implementation of `BackgroundTaskScheduler`. -final class BackgroundTaskScheduler_Mock: BackgroundTaskScheduler { - var isAppActive_called: Bool = false - var isAppActive_returns: Bool = true +final class BackgroundTaskScheduler_Mock: BackgroundTaskScheduler, @unchecked Sendable { + @Atomic var isAppActive_called: Bool = false + @Atomic var isAppActive_returns: Bool = true var isAppActive: Bool { isAppActive_called = true return isAppActive_returns } - var beginBackgroundTask_called: Bool = false - var beginBackgroundTask_expirationHandler: (() -> Void)? - var beginBackgroundTask_returns: Bool = true - func beginTask(expirationHandler: (() -> Void)?) -> Bool { + @Atomic var beginBackgroundTask_called: Bool = false + @Atomic var beginBackgroundTask_expirationHandler: (@MainActor () -> Void)? + @Atomic var beginBackgroundTask_returns: Bool = true + func beginTask(expirationHandler: (@MainActor () -> Void)?) -> Bool { beginBackgroundTask_called = true beginBackgroundTask_expirationHandler = expirationHandler return beginBackgroundTask_returns } - var endBackgroundTask_called: Bool = false + @Atomic var endBackgroundTask_called: Bool = false func endTask() { endBackgroundTask_called = true } - var startListeningForAppStateUpdates_called: Bool = false - var startListeningForAppStateUpdates_onBackground: (() -> Void)? - var startListeningForAppStateUpdates_onForeground: (() -> Void)? + @Atomic var startListeningForAppStateUpdates_called: Bool = false + @Atomic var startListeningForAppStateUpdates_onBackground: (() -> Void)? + @Atomic var startListeningForAppStateUpdates_onForeground: (() -> Void)? func startListeningForAppStateUpdates( onEnteringBackground: @escaping () -> Void, onEnteringForeground: @escaping () -> Void @@ -40,7 +40,7 @@ final class BackgroundTaskScheduler_Mock: BackgroundTaskScheduler { startListeningForAppStateUpdates_onForeground = onEnteringForeground } - var stopListeningForAppStateUpdates_called: Bool = false + @Atomic var stopListeningForAppStateUpdates_called: Bool = false func stopListeningForAppStateUpdates() { stopListeningForAppStateUpdates_called = true } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketClient_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketClient_Mock.swift index 71a7106d852..a6b2d0fbd87 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketClient_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketClient_Mock.swift @@ -6,23 +6,23 @@ import Foundation @testable import StreamChat /// Mock implementation of `WebSocketClient`. -final class WebSocketClient_Mock: WebSocketClient { +final class WebSocketClient_Mock: WebSocketClient, @unchecked Sendable { let init_sessionConfiguration: URLSessionConfiguration let init_requestEncoder: RequestEncoder let init_eventDecoder: AnyEventDecoder let init_eventNotificationCenter: EventNotificationCenter let init_environment: WebSocketClient.Environment - var connect_calledCounter = 0 + @Atomic var connect_calledCounter = 0 var connect_called: Bool { connect_calledCounter > 0 } - var disconnect_calledCounter = 0 - var disconnect_source: WebSocketConnectionState.DisconnectionSource? + @Atomic var disconnect_calledCounter = 0 + @Atomic var disconnect_source: WebSocketConnectionState.DisconnectionSource? var disconnect_called: Bool { disconnect_calledCounter > 0 } - var disconnect_completion: (() -> Void)? + @Atomic var disconnect_completion: (() -> Void)? - var mockedConnectionState: WebSocketConnectionState? + @Atomic var mockedConnectionState: WebSocketConnectionState? override var connectionState: WebSocketConnectionState { return mockedConnectionState ?? super.connectionState diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketEngine_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketEngine_Mock.swift index 92d55cef070..17090b204e2 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketEngine_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketEngine_Mock.swift @@ -5,11 +5,11 @@ import Foundation @testable import StreamChat -final class WebSocketEngine_Mock: WebSocketEngine { - var request: URLRequest - var sessionConfiguration: URLSessionConfiguration - var isConnected: Bool = false - var callbackQueue: DispatchQueue +final class WebSocketEngine_Mock: WebSocketEngine, @unchecked Sendable { + @Atomic var request: URLRequest + @Atomic var sessionConfiguration: URLSessionConfiguration + @Atomic var isConnected: Bool = false + @Atomic var callbackQueue: DispatchQueue weak var delegate: WebSocketEngineDelegate? /// How many times was `connect()` called diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketPingController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketPingController_Mock.swift index 2c7c01dc327..99df6b23648 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketPingController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/WebSocketPingController_Mock.swift @@ -6,9 +6,9 @@ import Foundation @testable import StreamChat import XCTest -final class WebSocketPingController_Mock: WebSocketPingController { - var connectionStateDidChange_connectionStates: [WebSocketConnectionState] = [] - var pongReceivedCount = 0 +final class WebSocketPingController_Mock: WebSocketPingController, @unchecked Sendable { + @Atomic var connectionStateDidChange_connectionStates: [WebSocketConnectionState] = [] + @Atomic var pongReceivedCount = 0 override func connectionStateDidChange(_ connectionState: WebSocketConnectionState) { connectionStateDidChange_connectionStates.append(connectionState) diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberListUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberListUpdater_Mock.swift index 91e5a74eb3c..be263b82b9e 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberListUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberListUpdater_Mock.swift @@ -6,16 +6,16 @@ import XCTest /// Mock implementation of `ChannelMemberListUpdater` -final class ChannelMemberListUpdater_Mock: ChannelMemberListUpdater { +final class ChannelMemberListUpdater_Mock: ChannelMemberListUpdater, @unchecked Sendable { @Atomic var load_query: ChannelMemberListQuery? - @Atomic var load_completion: ((Result<[ChatChannelMember], Error>) -> Void)? + @Atomic var load_completion: (@Sendable(Result<[ChatChannelMember], Error>) -> Void)? func cleanUp() { load_query = nil load_completion = nil } - override func load(_ query: ChannelMemberListQuery, completion: ((Result<[ChatChannelMember], Error>) -> Void)? = nil) { + override func load(_ query: ChannelMemberListQuery, completion: (@Sendable(Result<[ChatChannelMember], Error>) -> Void)? = nil) { load_query = query load_completion = completion } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift index e35ad9c6574..c764f361834 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// Mock implementation of `ChannelMemberUpdater` -final class ChannelMemberUpdater_Mock: ChannelMemberUpdater { +final class ChannelMemberUpdater_Mock: ChannelMemberUpdater, @unchecked Sendable { @Atomic var banMember_userId: UserId? @Atomic var banMember_cid: ChannelId? @Atomic var banMember_shadow: Bool? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift index 7f95c41bb53..71e45552e46 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// Mock implementation of ChannelUpdater -final class ChannelUpdater_Mock: ChannelUpdater { +final class ChannelUpdater_Mock: ChannelUpdater, @unchecked Sendable { @Atomic var update_channelQuery: ChannelQuery? @Atomic var update_onChannelCreated: ((ChannelId) -> Void)? @Atomic var update_completion: ((Result) -> Void)? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift index 9889c710bc3..73673630b3e 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// Mock implementation of `UserUpdater` -final class CurrentUserUpdater_Mock: CurrentUserUpdater { +final class CurrentUserUpdater_Mock: CurrentUserUpdater, @unchecked Sendable { @Atomic var updateUserData_currentUserId: UserId? @Atomic var updateUserData_name: String? @Atomic var updateUserData_imageURL: URL? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/EventNotificationCenter_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/EventNotificationCenter_Mock.swift index f2068ce8e98..f53d01e7bf2 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/EventNotificationCenter_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/EventNotificationCenter_Mock.swift @@ -12,15 +12,15 @@ final class EventNotificationCenter_Mock: EventNotificationCenter, @unchecked Se newMessageIdsMock ?? super.newMessageIds } - var newMessageIdsMock: Set? + @Atomic var newMessageIdsMock: Set? - lazy var mock_process = MockFunc<([Event], Bool, (() -> Void)?), Void>.mock(for: process) - var mock_processCalledWithEvents: [Event] = [] + lazy var mock_process = MockFunc<([Event], Bool, (@Sendable () -> Void)?), Void>.mock(for: process) + @Atomic var mock_processCalledWithEvents: [Event] = [] override func process( _ events: [Event], postNotifications: Bool = true, - completion: (() -> Void)? = nil + completion: (@Sendable() -> Void)? = nil ) { super.process(events, postNotifications: postNotifications, completion: completion) diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/EventSender_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/EventSender_Mock.swift index 3618401c1bf..f2db2b5ff6f 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/EventSender_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/EventSender_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// Mock implementation of `EventSender` -final class EventSender_Mock: EventSender { +final class EventSender_Mock: EventSender, @unchecked Sendable { @Atomic var sendEvent_payload: Any? @Atomic var sendEvent_cid: ChannelId? @Atomic var sendEvent_completion: ((Error?) -> Void)? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift index afa56d35bbe..8a1b4b6818a 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// Mock implementation of MessageUpdater -final class MessageUpdater_Mock: MessageUpdater { +final class MessageUpdater_Mock: MessageUpdater, @unchecked Sendable { @Atomic var getMessage_cid: ChannelId? @Atomic var getMessage_messageId: MessageId? @Atomic var getMessage_completion: ((Result) -> Void)? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ThreadsRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ThreadsRepository_Mock.swift index 6f17df4e715..901616dde86 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ThreadsRepository_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ThreadsRepository_Mock.swift @@ -6,7 +6,7 @@ @testable import StreamChatTestTools import XCTest -final class ThreadsRepository_Mock: ThreadsRepository { +final class ThreadsRepository_Mock: ThreadsRepository, @unchecked Sendable { init() { super.init( database: DatabaseContainer_Spy(), diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/TypingEventsSender_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/TypingEventsSender_Mock.swift index 871a7b86a9d..b37f9932fd0 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/TypingEventsSender_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/TypingEventsSender_Mock.swift @@ -7,7 +7,7 @@ import Foundation import XCTest /// Mock implementation of ChannelUpdater -final class TypingEventsSender_Mock: TypingEventsSender { +final class TypingEventsSender_Mock: TypingEventsSender, @unchecked Sendable { @Atomic var keystroke_cid: ChannelId? @Atomic var keystroke_parentMessageId: MessageId? @Atomic var keystroke_completion: ((Error?) -> Void)? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserListUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserListUpdater_Mock.swift index ed83ccc3581..9458478417a 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserListUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserListUpdater_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// Mock implementation of UserListUpdater -final class UserListUpdater_Mock: UserListUpdater { +final class UserListUpdater_Mock: UserListUpdater, @unchecked Sendable { @Atomic var update_queries: [UserListQuery] = [] @Atomic var update_policy: UpdatePolicy? @Atomic var update_completion: ((Result<[ChatUser], Error>) -> Void)? diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserUpdater_Mock.swift index eee1979db2f..d39fe611579 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserUpdater_Mock.swift @@ -6,7 +6,7 @@ import XCTest /// Mock implementation of `UserUpdater` -final class UserUpdater_Mock: UserUpdater { +final class UserUpdater_Mock: UserUpdater, @unchecked Sendable { @Atomic var muteUser_userId: UserId? @Atomic var muteUser_completion: ((Error?) -> Void)? @Atomic var muteUser_completion_result: Result? diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift index 9e8f1825026..fd32f49579c 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift @@ -7,7 +7,7 @@ import Foundation import XCTest /// Mock implementation of APIClient allowing easy control and simulation of responses. -final class APIClient_Spy: APIClient, Spy { +final class APIClient_Spy: APIClient, Spy, @unchecked Sendable { enum Signature { static let flushRequestsQueue = "flushRequestsQueue()" } diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentDownloader_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentDownloader_Spy.swift index 0720cc8202e..332e4ea0ff0 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentDownloader_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentDownloader_Spy.swift @@ -10,7 +10,7 @@ final class AttachmentDownloader_Spy: AttachmentDownloader, Spy { @Atomic var downloadAttachmentProgress: Double? @Atomic var downloadAttachmentResult: Error? - func download(from remoteURL: URL, to localURL: URL, progress: ((Double) -> Void)?, completion: @escaping ((any Error)?) -> Void) { + func download(from remoteURL: URL, to localURL: URL, progress: (@Sendable(Double) -> Void)?, completion: @escaping @Sendable((any Error)?) -> Void) { record() if let downloadAttachmentProgress { progress?(downloadAttachmentProgress) diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift index 440f8cbb826..6cd30d57dab 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift @@ -5,16 +5,16 @@ import Foundation import StreamChat -final class AttachmentUploader_Spy: AttachmentUploader, Spy { +final class AttachmentUploader_Spy: AttachmentUploader, Spy, @unchecked Sendable { let spyState = SpyState() - var uploadAttachmentProgress: Double? - var uploadAttachmentResult: Result? + @Atomic var uploadAttachmentProgress: Double? + @Atomic var uploadAttachmentResult: Result? func upload( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Result) -> Void ) { record() diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift index c03fc7fb0b4..4b2de876995 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift @@ -5,17 +5,17 @@ import StreamChat import Foundation -final class CDNClient_Spy: CDNClient, Spy { +final class CDNClient_Spy: CDNClient, Spy, @unchecked Sendable { let spyState = SpyState() static var maxAttachmentSize: Int64 { .max } - var uploadAttachmentProgress: Double? - var uploadAttachmentResult: Result? + @Atomic var uploadAttachmentProgress: Double? + @Atomic var uploadAttachmentResult: Result? func uploadAttachment( _ attachment: AnyChatMessageAttachment, - progress: ((Double) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@Sendable(Double) -> Void)?, + completion: @escaping @Sendable(Result) -> Void ) { record() if let uploadAttachmentProgress = uploadAttachmentProgress { diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift index fe9aa5510ba..732d5016be4 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift @@ -6,7 +6,7 @@ import XCTest /// Mock implementation of ChannelListUpdater -final class ChannelListUpdater_Spy: ChannelListUpdater, Spy { +final class ChannelListUpdater_Spy: ChannelListUpdater, Spy, @unchecked Sendable { let spyState = SpyState() @Atomic var update_queries: [ChannelListQuery] = [] @@ -20,14 +20,14 @@ final class ChannelListUpdater_Spy: ChannelListUpdater, Spy { @Atomic var markAllRead_completion: ((Error?) -> Void)? - var startWatchingChannels_callCount = 0 + @Atomic var startWatchingChannels_callCount = 0 @Atomic var startWatchingChannels_cids: [ChannelId] = [] @Atomic var startWatchingChannels_completion: ((Error?) -> Void)? - var link_callCount = 0 - var link_completion: ((Error?) -> Void)? + @Atomic var link_callCount = 0 + @Atomic var link_completion: ((Error?) -> Void)? - var unlink_callCount = 0 + @Atomic var unlink_callCount = 0 func cleanUp() { update_queries.removeAll() diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/ChatChannelController_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/ChatChannelController_Spy.swift index 7bf1067d16c..4e919b5e5a8 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/ChatChannelController_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/ChatChannelController_Spy.swift @@ -5,8 +5,8 @@ import Foundation @testable import StreamChat -final class ChatChannelController_Spy: ChatChannelController, Spy { - var watchActiveChannelError: Error? +final class ChatChannelController_Spy: ChatChannelController, Spy, @unchecked Sendable { + @Atomic var watchActiveChannelError: Error? let spyState = SpyState() init(client: ChatClient_Mock) { @@ -19,20 +19,20 @@ final class ChatChannelController_Spy: ChatChannelController, Spy { } } -final class ChannelControllerSpy: ChatChannelController { +final class ChannelControllerSpy: ChatChannelController, @unchecked Sendable { @Atomic var synchronize_called = false - var channel_simulated: ChatChannel? + @Atomic var channel_simulated: ChatChannel? override var channel: ChatChannel? { channel_simulated } - var messages_simulated: [ChatMessage]? + @Atomic var messages_simulated: [ChatMessage]? override var messages: LazyCachedMapCollection { messages_simulated.map { $0.lazyCachedMap { $0 } } ?? super.messages } - var state_simulated: DataController.State? + @Atomic var state_simulated: DataController.State? override var state: DataController.State { get { state_simulated ?? super.state } set { super.state = newValue } diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift index 48e5b32ddd6..5e39119b094 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift @@ -127,15 +127,15 @@ public final class DatabaseContainer_Spy: DatabaseContainer, Spy, @unchecked Sen super.recreatePersistentStore(completion: completion) } - override public func write(_ actions: @escaping (DatabaseSession) throws -> Void, completion: @escaping (Error?) -> Void) { + override public func write(_ actions: @escaping @Sendable(DatabaseSession) throws -> Void, completion: @escaping @Sendable(Error?) -> Void) { record() - let wrappedActions: ((DatabaseSession) throws -> Void) = { session in + let wrappedActions: (@Sendable(DatabaseSession) throws -> Void) = { session in self.isWriteSessionInProgress = true try actions(self.sessionMock ?? session) self.isWriteSessionInProgress = false } - let completion: (Error?) -> Void = { error in + let completion: @Sendable(Error?) -> Void = { error in completion(error) self._writeSessionCounter { $0 += 1 } self.didWrite?() @@ -159,7 +159,7 @@ public final class DatabaseContainer_Spy: DatabaseContainer, Spy, @unchecked Sen extension DatabaseContainer { /// Reads changes from the DB synchronously. Only for test purposes! - func readSynchronously(_ actions: @escaping (DatabaseSession) throws -> T) throws -> T { + func readSynchronously(_ actions: @escaping @Sendable (DatabaseSession) throws -> T) throws -> T { let result = try waitFor { completion in self.read(actions, completion: completion) } @@ -172,7 +172,7 @@ extension DatabaseContainer { } /// Writes changes to the DB synchronously. Only for test purposes! - func writeSynchronously(_ actions: @escaping (DatabaseSession) throws -> Void) throws { + func writeSynchronously(_ actions: @escaping @Sendable(DatabaseSession) throws -> Void) throws { let error = try waitFor { completion in self.write(actions, completion: completion) } @@ -207,7 +207,6 @@ extension DatabaseContainer { withMessages: Bool = true, withQuery: Bool = false, isHidden: Bool = false, - channelReads: Set = [], channelExtraData: [String: RawJSON] = [:], truncatedAt: Date? = nil ) throws { @@ -219,7 +218,6 @@ extension DatabaseContainer { ) dto.isHidden = isHidden - dto.reads = channelReads // Delete possible messages from the payload if `withMessages` is false if !withMessages { let context = session as! NSManagedObjectContext @@ -277,7 +275,6 @@ extension DatabaseContainer { id: MessageId = .unique, authorId: UserId = .unique, cid: ChannelId = .unique, - channel: ChannelDTO? = nil, text: String = .unique, extraData: [String: RawJSON] = [:], pinned: Bool = false, @@ -297,8 +294,7 @@ extension DatabaseContainer { quotedMessageId: MessageId? = nil ) throws { try writeSynchronously { session in - guard let channelDTO = channel ?? - (try? session.saveChannel(payload: XCTestCase().dummyPayload(with: cid, numberOfMessages: 0))) else { + guard let channelDTO = (try? session.saveChannel(payload: XCTestCase().dummyPayload(with: cid, numberOfMessages: 0))) else { XCTFail("Failed to fetch channel when creating message") return } diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/Logger_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/Logger_Spy.swift index 5a0d5a5a725..5d2dfb7a582 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/Logger_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/Logger_Spy.swift @@ -5,9 +5,9 @@ import Foundation @testable import StreamChat -final class Logger_Spy: Logger, Spy { +final class Logger_Spy: Logger, Spy, @unchecked Sendable { let spyState = SpyState() - var originalLogger: Logger? + @Atomic var originalLogger: Logger? @Atomic var failedAsserts: Int = 0 func injectMock() { diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/RequestDecoder_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/RequestDecoder_Spy.swift index 17e9bf19ef7..3354c6e3795 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/RequestDecoder_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/RequestDecoder_Spy.swift @@ -6,15 +6,15 @@ import Foundation @testable import StreamChat import XCTest -final class RequestDecoder_Spy: RequestDecoder, Spy { +final class RequestDecoder_Spy: RequestDecoder, Spy, @unchecked Sendable { let spyState = SpyState() - var decodeRequestResponse: Result? - var decodeRequestDelay: TimeInterval? + @Atomic var decodeRequestResponse: Result? + @Atomic var decodeRequestDelay: TimeInterval? - var decodeRequestResponse_data: Data? - var decodeRequestResponse_response: HTTPURLResponse? - var decodeRequestResponse_error: Error? - var onDecodeRequestResponseCall: (() -> Void)? + @Atomic var decodeRequestResponse_data: Data? + @Atomic var decodeRequestResponse_response: HTTPURLResponse? + @Atomic var decodeRequestResponse_error: Error? + @Atomic var onDecodeRequestResponseCall: (() -> Void)? func decodeRequestResponse( data: Data?, diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/RequestEncoder_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/RequestEncoder_Spy.swift index 44e67f1920c..b3b4d52be7c 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/RequestEncoder_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/RequestEncoder_Spy.swift @@ -6,17 +6,17 @@ import Foundation @testable import StreamChat import XCTest -final class RequestEncoder_Spy: RequestEncoder, Spy { +final class RequestEncoder_Spy: RequestEncoder, Spy, @unchecked Sendable { let spyState = SpyState() let init_baseURL: URL let init_apiKey: APIKey weak var connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate? - var encodeRequest: Result? = .success(URLRequest(url: .unique())) - var onEncodeRequestCall: (() -> Void)? - var encodeRequest_endpoints: [AnyEndpoint] = [] - var encodeRequest_completion: ((Result) -> Void)? + @Atomic var encodeRequest: Result? = .success(URLRequest(url: .unique())) + @Atomic var onEncodeRequestCall: (() -> Void)? + @Atomic var encodeRequest_endpoints: [AnyEndpoint] = [] + @Atomic var encodeRequest_completion: ((Result) -> Void)? func encodeRequest( for endpoint: Endpoint, diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/RetryStrategy_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/RetryStrategy_Spy.swift index f633d22c57c..82fea14bdf1 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/RetryStrategy_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/RetryStrategy_Spy.swift @@ -6,14 +6,14 @@ import Foundation @testable import StreamChat /// Mock implementation of `RetryStrategy`. -final class RetryStrategy_Spy: RetryStrategy, Spy { +final class RetryStrategy_Spy: RetryStrategy, Spy, @unchecked Sendable { enum Signature { static let nextRetryDelay = "nextRetryDelay()" static let resetConsecutiveFailures = "resetConsecutiveFailures()" } let spyState = SpyState() - var consecutiveFailuresCount: Int = 0 + @Atomic var consecutiveFailuresCount: Int = 0 lazy var mock_incrementConsecutiveFailures = MockFunc.mock(for: incrementConsecutiveFailures) diff --git a/TestTools/StreamChatTestTools/TestData/FilterTestScope.swift b/TestTools/StreamChatTestTools/TestData/FilterTestScope.swift index 221a1bbb41f..80394c318eb 100644 --- a/TestTools/StreamChatTestTools/TestData/FilterTestScope.swift +++ b/TestTools/StreamChatTestTools/TestData/FilterTestScope.swift @@ -50,7 +50,7 @@ struct FilterCodingTestPair { var json: String var filter: Filter - static var allCases: [FilterCodingTestPair] = [ + static let allCases: [FilterCodingTestPair] = [ .equalInt(), .notEqualDate(), .greaterDouble(), diff --git a/TestTools/StreamChatTestTools/TestData/PhotoMetaData.swift b/TestTools/StreamChatTestTools/TestData/PhotoMetaData.swift index f4e4dc56d33..fd2487cb38e 100644 --- a/TestTools/StreamChatTestTools/TestData/PhotoMetaData.swift +++ b/TestTools/StreamChatTestTools/TestData/PhotoMetaData.swift @@ -5,8 +5,8 @@ import Foundation @testable import StreamChat -public struct PhotoMetadata: Codable, Equatable { - public struct Location: Codable, Equatable { +public struct PhotoMetadata: Codable, Equatable, Sendable { + public struct Location: Codable, Equatable, Sendable { public let longitude: Double public let latitude: Double } diff --git a/TestTools/StreamChatTestTools/TestData/TestUser.swift b/TestTools/StreamChatTestTools/TestData/TestUser.swift index e12ebde67e3..56f04724a21 100644 --- a/TestTools/StreamChatTestTools/TestData/TestUser.swift +++ b/TestTools/StreamChatTestTools/TestData/TestUser.swift @@ -4,7 +4,7 @@ import Foundation -public struct TestUser: Codable, Equatable { +public struct TestUser: Codable, Equatable, Sendable { public let name: String public let age: Int diff --git a/TestTools/StreamChatTestTools/VirtualTime/VirtualTime.swift b/TestTools/StreamChatTestTools/VirtualTime/VirtualTime.swift index 5a208bd2c7c..e9c7cfec436 100644 --- a/TestTools/StreamChatTestTools/VirtualTime/VirtualTime.swift +++ b/TestTools/StreamChatTestTools/VirtualTime/VirtualTime.swift @@ -3,6 +3,7 @@ // import Foundation +@testable import StreamChat import XCTest /// This class allows simulating time-based events in tests. @@ -90,12 +91,12 @@ final class VirtualTime { extension VirtualTime { /// Internal representation of a timer scheduled with `VirtualTime`. Not meant to be used directly. - class TimerControl { - private(set) var isActive = true + class TimerControl: @unchecked Sendable { + @Atomic private(set) var isActive = true - var repeatingPeriod: TimeInterval - var scheduledFireTime: TimeInterval - var callback: (TimerControl) -> Void + @Atomic var repeatingPeriod: TimeInterval + @Atomic var scheduledFireTime: TimeInterval + @Atomic var callback: (TimerControl) -> Void var isRepeated: Bool { repeatingPeriod > 0 diff --git a/TestTools/StreamChatTestTools/VirtualTime/VirtualTimer.swift b/TestTools/StreamChatTestTools/VirtualTime/VirtualTimer.swift index 5ea506a8710..1f172b11fc0 100644 --- a/TestTools/StreamChatTestTools/VirtualTime/VirtualTimer.swift +++ b/TestTools/StreamChatTestTools/VirtualTime/VirtualTimer.swift @@ -6,15 +6,17 @@ import Foundation @testable import StreamChat struct VirtualTimeTimer: StreamChat.Timer { - static var time: VirtualTime! + static let time = AllocatedUnfairLock(nil) static func invalidate() { - time.invalidate() - time = nil + time.withLock { + $0?.invalidate() + $0 = nil + } } static func schedule(timeInterval: TimeInterval, queue: DispatchQueue, onFire: @escaping () -> Void) -> TimerControl { - Self.time.scheduleTimer( + Self.time.value!.scheduleTimer( interval: timeInterval, repeating: false, callback: { _ in onFire() } @@ -26,7 +28,7 @@ struct VirtualTimeTimer: StreamChat.Timer { queue: DispatchQueue, onFire: @escaping () -> Void ) -> RepeatingTimerControl { - Self.time.scheduleTimer( + Self.time.value!.scheduleTimer( interval: timeInterval, repeating: true, callback: { _ in onFire() } @@ -34,7 +36,7 @@ struct VirtualTimeTimer: StreamChat.Timer { } static func currentTime() -> Date { - Date(timeIntervalSinceReferenceDate: time.currentTime) + Date(timeIntervalSinceReferenceDate: time.value!.currentTime) } } diff --git a/TestTools/StreamChatTestTools/Wait/WaitFor.swift b/TestTools/StreamChatTestTools/Wait/WaitFor.swift index 18c1882ba4c..c5d063c7316 100644 --- a/TestTools/StreamChatTestTools/Wait/WaitFor.swift +++ b/TestTools/StreamChatTestTools/Wait/WaitFor.swift @@ -33,10 +33,10 @@ public func waitFor( timeout: TimeInterval = waitForTimeout, file: StaticString = #filePath, line: UInt = #line, - _ action: (_ done: @escaping (T) -> Void) -> Void + _ action: (_ done: @escaping @Sendable(T) -> Void) -> Void ) throws -> T { let expectation = XCTestExpectation(description: "Action completed") - var result: T? + nonisolated(unsafe) var result: T? action { resultValue in result = resultValue if Thread.isMainThread { diff --git a/Tests/StreamChatTests/APIClient/APIClient_Tests.swift b/Tests/StreamChatTests/APIClient/APIClient_Tests.swift index 76851abec9f..c3b2e18cc02 100644 --- a/Tests/StreamChatTests/APIClient/APIClient_Tests.swift +++ b/Tests/StreamChatTests/APIClient/APIClient_Tests.swift @@ -131,7 +131,7 @@ final class APIClient_Tests: XCTestCase { encoder.onEncodeRequestCall = { firstEncodeRequestExpectation.fulfill() } - var result: Result? + nonisolated(unsafe) var result: Result? apiClient.request(endpoint: testEndpoint) { result = $0 requestCompletesExpectation.fulfill() @@ -357,8 +357,8 @@ final class APIClient_Tests: XCTestCase { ) ) - var receivedProgress: Double? - var receivedResult: Result? + nonisolated(unsafe) var receivedProgress: Double? + nonisolated(unsafe) var receivedResult: Result? waitUntil(timeout: defaultTimeout) { done in apiClient.uploadAttachment( attachment, @@ -379,8 +379,8 @@ final class APIClient_Tests: XCTestCase { let networkError = NSError(domain: "", code: NSURLErrorNotConnectedToInternet, userInfo: nil) attachmentUploader.uploadAttachmentResult = .failure(networkError) - var receivedProgress: Double? - var receivedResult: Result? + nonisolated(unsafe) var receivedProgress: Double? + nonisolated(unsafe) var receivedResult: Result? let expectation = self.expectation(description: "Upload completes") apiClient.uploadAttachment( attachment, @@ -402,8 +402,8 @@ final class APIClient_Tests: XCTestCase { let error = NSError(domain: "", code: 1, userInfo: nil) attachmentUploader.uploadAttachmentResult = .failure(error) - var receivedProgress: Double? - var receivedResult: Result? + nonisolated(unsafe) var receivedProgress: Double? + nonisolated(unsafe) var receivedResult: Result? let expectation = self.expectation(description: "Upload completes") apiClient.uploadAttachment( attachment, @@ -447,7 +447,7 @@ final class APIClient_Tests: XCTestCase { let encoderError = ClientError.ExpiredToken() decoder.decodeRequestResponse = .failure(encoderError) - var result: Result? + nonisolated(unsafe) var result: Result? apiClient.request( endpoint: Endpoint.mock(), completion: { @@ -622,7 +622,7 @@ final class APIClient_Tests: XCTestCase { // Put 5 requests on the queue. Only one should be executed at a time let lastRequestExpectation = expectation(description: "Last request completed") - var results: [Result] = [] + nonisolated(unsafe) var results: [Result] = [] (1...5).forEach { index in let channelId = ChannelId(type: .messaging, id: "\(index)") self.apiClient.recoveryRequest(endpoint: Endpoint.mock(path: .sendMessage(channelId))) { result in diff --git a/Tests/StreamChatTests/APIClient/RequestEncoder_Tests.swift b/Tests/StreamChatTests/APIClient/RequestEncoder_Tests.swift index 37006d5e208..95e537ccfd7 100644 --- a/Tests/StreamChatTests/APIClient/RequestEncoder_Tests.swift +++ b/Tests/StreamChatTests/APIClient/RequestEncoder_Tests.swift @@ -22,7 +22,7 @@ final class RequestEncoder_Tests: XCTestCase { connectionDetailsProvider = ConnectionDetailsProviderDelegate_Spy() encoder.connectionDetailsProviderDelegate = connectionDetailsProvider - VirtualTimeTimer.time = VirtualTime() + VirtualTimeTimer.time.value = VirtualTime() } override func tearDown() { @@ -107,7 +107,7 @@ final class RequestEncoder_Tests: XCTestCase { connectionDetailsProvider.provideTokenResult = nil // Encode the request and capture the result - var encodingResult: Result? + nonisolated(unsafe) var encodingResult: Result? encoder.encodeRequest(for: endpoint) { encodingResult = $0 } // Cancel all token waiting requests. @@ -130,7 +130,7 @@ final class RequestEncoder_Tests: XCTestCase { connectionDetailsProvider.provideTokenResult = .failure(ClientError.WaiterTimeout()) // Encode the request and capture the result - var encodingResult: Result? + nonisolated(unsafe) var encodingResult: Result? encoder.encodeRequest(for: endpoint) { encodingResult = $0 } // Assert request encoding has failed with the correct error. @@ -150,7 +150,7 @@ final class RequestEncoder_Tests: XCTestCase { connectionDetailsProvider.provideTokenResult = .failure(TestError()) // Encode the request and capture the result - var encodingResult: Result? + nonisolated(unsafe) var encodingResult: Result? encoder.encodeRequest(for: endpoint) { encodingResult = $0 } // Assert request encoding has failed with the correct error. @@ -195,7 +195,7 @@ final class RequestEncoder_Tests: XCTestCase { connectionDetailsProvider.provideConnectionIdResult = nil // Encode the request and capture the result - var encodingResult: Result? + nonisolated(unsafe) var encodingResult: Result? encoder.encodeRequest(for: endpoint) { encodingResult = $0 } // Cancel all connection id waiting requests. @@ -220,7 +220,7 @@ final class RequestEncoder_Tests: XCTestCase { connectionDetailsProvider.provideConnectionIdResult = .failure(ClientError.WaiterTimeout()) // Encode the request and capture the result - var encodingResult: Result? + nonisolated(unsafe) var encodingResult: Result? encoder.encodeRequest(for: endpoint) { encodingResult = $0 } // Assert request encoding has failed with the correct error. @@ -242,7 +242,7 @@ final class RequestEncoder_Tests: XCTestCase { connectionDetailsProvider.provideConnectionIdResult = .failure(TestError()) // Encode the request and capture the result - var encodingResult: Result? + nonisolated(unsafe) var encodingResult: Result? encoder.encodeRequest(for: endpoint) { encodingResult = $0 } // Assert request encoding has failed with the correct error. diff --git a/Tests/StreamChatTests/APIClient/StreamAttachmentUploader_Tests.swift b/Tests/StreamChatTests/APIClient/StreamAttachmentUploader_Tests.swift index e97d2450958..f61e4645507 100644 --- a/Tests/StreamChatTests/APIClient/StreamAttachmentUploader_Tests.swift +++ b/Tests/StreamChatTests/APIClient/StreamAttachmentUploader_Tests.swift @@ -16,7 +16,7 @@ final class StreamAttachmentUploader_Tests: XCTestCase { let mockedAttachment = ChatMessageFileAttachment.mock( id: .init(cid: .unique, messageId: .unique, index: .unique) ) - let mockProgress: ((Double) -> Void) = { + let mockProgress: (@Sendable(Double) -> Void) = { XCTAssertEqual($0, expectedProgress) expProgressCalled.fulfill() } diff --git a/Tests/StreamChatTests/Audio/StreamAssetPropertyLoader_Tests.swift b/Tests/StreamChatTests/Audio/StreamAssetPropertyLoader_Tests.swift index d61ad2ad228..1c60bc02583 100644 --- a/Tests/StreamChatTests/Audio/StreamAssetPropertyLoader_Tests.swift +++ b/Tests/StreamChatTests/Audio/StreamAssetPropertyLoader_Tests.swift @@ -228,7 +228,7 @@ final class StreamAssetPropertyLoader_Tests: XCTestCase { file: StaticString = #file, line: UInt = #line ) throws { - var completionWasCalledWithResult: Result? + nonisolated(unsafe) var completionWasCalledWithResult: Result? mockAsset.statusOfValueResultMap = statusOfValueResultMap subject.loadProperties( diff --git a/Tests/StreamChatTests/Audio/StreamAudioRecorder_Tests.swift b/Tests/StreamChatTests/Audio/StreamAudioRecorder_Tests.swift index 943208f7898..2c26be69845 100644 --- a/Tests/StreamChatTests/Audio/StreamAudioRecorder_Tests.swift +++ b/Tests/StreamChatTests/Audio/StreamAudioRecorder_Tests.swift @@ -449,7 +449,7 @@ final class StreamAudioRecorder_Tests: XCTestCase { } } -private final class MockΑudioRecorderMeterNormaliser: AudioValuePercentageNormaliser { +private final class MockΑudioRecorderMeterNormaliser: AudioValuePercentageNormaliser, @unchecked Sendable { private(set) var normaliseWasCalledWithValue: Float? var normaliseResult: Float = 0 diff --git a/Tests/StreamChatTests/Audio/StreamAudioWaveformAnalyser_Tests.swift b/Tests/StreamChatTests/Audio/StreamAudioWaveformAnalyser_Tests.swift index d8b5c85578b..4e2eaea377a 100644 --- a/Tests/StreamChatTests/Audio/StreamAudioWaveformAnalyser_Tests.swift +++ b/Tests/StreamChatTests/Audio/StreamAudioWaveformAnalyser_Tests.swift @@ -111,7 +111,7 @@ final class StreamAudioWaveformAnalyser_Tests: XCTestCase { } } -private final class SpyAudioSamplesExtractor: AudioSamplesExtractor { +private final class SpyAudioSamplesExtractor: AudioSamplesExtractor, @unchecked Sendable { private(set) var extractSamplesWasCalledWithReadSampleBuffer: CMSampleBuffer? private(set) var extractSamplesWasCalledWithSampleBuffer: Data? private(set) var extractSamplesWasCalledWithDownsamplingRate: Int? @@ -134,7 +134,7 @@ private final class SpyAudioSamplesExtractor: AudioSamplesExtractor { } } -private final class SpyAudioSamplesProcessor: AudioSamplesProcessor { +private final class SpyAudioSamplesProcessor: AudioSamplesProcessor, @unchecked Sendable { private(set) var processSamplesWasCalledWithSampleBuffer: Data? private(set) var processSamplesWasCalledWithOutputSamples: [Float]? private(set) var processSamplesWasCalledWithSamplesToProcess: Int? @@ -170,7 +170,7 @@ private final class SpyAudioSamplesProcessor: AudioSamplesProcessor { } } -private final class SpyAudioSamplesPercentageTransformer: AudioValuePercentageNormaliser { +private final class SpyAudioSamplesPercentageTransformer: AudioValuePercentageNormaliser, @unchecked Sendable { private(set) var transformWasCalledWithSamples: [Float]? private(set) var timesTransformWasCalled: Int = 0 diff --git a/Tests/StreamChatTests/BackgroundListDatabaseObserver_Tests.swift b/Tests/StreamChatTests/BackgroundListDatabaseObserver_Tests.swift index 868ea6ee9cf..26ca89ba90e 100644 --- a/Tests/StreamChatTests/BackgroundListDatabaseObserver_Tests.swift +++ b/Tests/StreamChatTests/BackgroundListDatabaseObserver_Tests.swift @@ -123,7 +123,7 @@ final class BackgroundListDatabaseObserver_Tests: XCTestCase { let onDidChangeExpectation = expectation(description: "onDidChange") onDidChangeExpectation.expectedFulfillmentCount = 2 - var receivedChanges: [ListChange] = [] + nonisolated(unsafe) var receivedChanges: [ListChange] = [] observer.onDidChange = { receivedChanges.append(contentsOf: $0) onDidChangeExpectation.fulfill() @@ -131,7 +131,7 @@ final class BackgroundListDatabaseObserver_Tests: XCTestCase { // Insert the test object let testValue = String.unique - var item: TestManagedObject! + nonisolated(unsafe) var item: TestManagedObject! try database.writeSynchronously { _ in let context = self.database.writableContext item = NSEntityDescription.insertNewObject(forEntityName: "TestManagedObject", into: context) as? TestManagedObject diff --git a/Tests/StreamChatTests/ChatClient_Tests.swift b/Tests/StreamChatTests/ChatClient_Tests.swift index d09aac6b6fd..b8434f4d094 100644 --- a/Tests/StreamChatTests/ChatClient_Tests.swift +++ b/Tests/StreamChatTests/ChatClient_Tests.swift @@ -33,7 +33,7 @@ final class ChatClient_Tests: XCTestCase { userId = .unique testEnv = .init() time = VirtualTime() - VirtualTimeTimer.time = time + VirtualTimeTimer.time.value = time } override func tearDown() { @@ -83,42 +83,29 @@ final class ChatClient_Tests: XCTestCase { config.deletedMessagesVisibility = .alwaysVisible config.shouldShowShadowedMessages = .random() - var usedDatabaseKind: DatabaseContainer.Kind? - var shouldFlushDBOnStart: Bool? - var shouldResetEphemeralValues: Bool? - var localCachingSettings: ChatClientConfig.LocalCaching? - var deleteMessagesVisibility: ChatClientConfig.DeletedMessageVisibility? - var shouldShowShadowedMessages: Bool? - // Create env object with custom database builder var env = ChatClient.Environment() - env.connectionRepositoryBuilder = ConnectionRepository_Mock.init - env - .databaseContainerBuilder = { kind, clientConfig in - usedDatabaseKind = kind - shouldFlushDBOnStart = clientConfig.shouldFlushLocalStorageOnStart - shouldResetEphemeralValues = clientConfig.isClientInActiveMode - localCachingSettings = clientConfig.localCaching - deleteMessagesVisibility = clientConfig.deletedMessagesVisibility - shouldShowShadowedMessages = clientConfig.shouldShowShadowedMessages - return DatabaseContainer_Spy() - } + env.connectionRepositoryBuilder = { + ConnectionRepository_Mock(isClientInActiveMode: $0, syncRepository: $1, webSocketClient: $2, apiClient: $3, timerType: $4) + } + env.databaseContainerBuilder = { [config] kind, clientConfig in + XCTAssertEqual( + kind, + .onDisk(databaseFileURL: storeFolderURL.appendingPathComponent(config.apiKey.apiKeyString)) + ) + XCTAssertEqual(clientConfig.shouldFlushLocalStorageOnStart, config.shouldFlushLocalStorageOnStart) + XCTAssertEqual(clientConfig.isClientInActiveMode, config.isClientInActiveMode) + XCTAssertEqual(clientConfig.localCaching, config.localCaching) + XCTAssertEqual(clientConfig.deletedMessagesVisibility, config.deletedMessagesVisibility) + XCTAssertEqual(clientConfig.shouldShowShadowedMessages, config.shouldShowShadowedMessages) + return DatabaseContainer_Spy() + } // Create a `Client` and assert that a DB file is created on the provided URL + APIKey path _ = ChatClient( config: config, environment: env ) - - XCTAssertEqual( - usedDatabaseKind, - .onDisk(databaseFileURL: storeFolderURL.appendingPathComponent(config.apiKey.apiKeyString)) - ) - XCTAssertEqual(shouldFlushDBOnStart, config.shouldFlushLocalStorageOnStart) - XCTAssertEqual(shouldResetEphemeralValues, config.isClientInActiveMode) - XCTAssertEqual(localCachingSettings, config.localCaching) - XCTAssertEqual(deleteMessagesVisibility, config.deletedMessagesVisibility) - XCTAssertEqual(shouldShowShadowedMessages, config.shouldShowShadowedMessages) } func test_clientDatabaseStackInitialization_whenLocalStorageDisabled() { @@ -126,13 +113,13 @@ final class ChatClient_Tests: XCTestCase { var config = ChatClientConfig() config.isLocalStorageEnabled = false - var usedDatabaseKind: DatabaseContainer.Kind? - // Create env object with custom database builder var env = ChatClient.Environment() - env.connectionRepositoryBuilder = ConnectionRepository_Mock.init + env.connectionRepositoryBuilder = { + ConnectionRepository_Mock(isClientInActiveMode: $0, syncRepository: $1, webSocketClient: $2, apiClient: $3, timerType: $4) + } env.databaseContainerBuilder = { kind, _ in - usedDatabaseKind = kind + XCTAssertEqual(kind, .inMemory) return DatabaseContainer_Spy() } @@ -141,8 +128,6 @@ final class ChatClient_Tests: XCTestCase { config: config, environment: env ) - - XCTAssertEqual(usedDatabaseKind, .inMemory) } /// When the initialization of a local DB fails for some reason (i.e. incorrect URL), @@ -153,13 +138,13 @@ final class ChatClient_Tests: XCTestCase { config.isLocalStorageEnabled = true config.localStorageFolderURL = nil - var usedDatabaseKinds: [DatabaseContainer.Kind] = [] - // Create env object and store all `kinds it's called with. var env = ChatClient.Environment() - env.connectionRepositoryBuilder = ConnectionRepository_Mock.init + env.connectionRepositoryBuilder = { + ConnectionRepository_Mock(isClientInActiveMode: $0, syncRepository: $1, webSocketClient: $2, apiClient: $3, timerType: $4) + } env.databaseContainerBuilder = { kind, _ in - usedDatabaseKinds.append(kind) + XCTAssertEqual(.inMemory, kind) return DatabaseContainer_Spy() } @@ -169,11 +154,6 @@ final class ChatClient_Tests: XCTestCase { config: config, environment: env ) - - XCTAssertEqual( - usedDatabaseKinds, - [.inMemory] - ) } // MARK: - WebSocketClient tests @@ -526,15 +506,11 @@ final class ChatClient_Tests: XCTestCase { let testError = TestError() let userInfo = UserInfo(id: "id") let authenticationRepository = try XCTUnwrap(client.authenticationRepository as? AuthenticationRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectUserResult = .failure(testError) - var receivedError: Error? - client.connectUser(userInfo: userInfo, tokenProvider: { _ in }) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectUser(userInfo: userInfo, tokenProvider: { _ in }, completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectTokenProvider, on: authenticationRepository) XCTAssertEqual(receivedError, testError) @@ -547,15 +523,11 @@ final class ChatClient_Tests: XCTestCase { let reconnectionTimeoutHandler = try XCTUnwrap(client.reconnectionTimeoutHandler as? ScheduledStreamTimer_Mock) let connectionRecoveryHandler = try XCTUnwrap(client.connectionRecoveryHandler as? ConnectionRecoveryHandler_Mock) let connectionRepository = try XCTUnwrap(client.connectionRepository as? ConnectionRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectUserResult = .success(()) - var receivedError: Error? - client.connectUser(userInfo: userInfo, tokenProvider: { _ in }) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectUser(userInfo: userInfo, tokenProvider: { _ in }, completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertNil(receivedError) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectTokenProvider, on: authenticationRepository) @@ -571,15 +543,11 @@ final class ChatClient_Tests: XCTestCase { let testError = TestError() let userInfo = UserInfo(id: "id") let authenticationRepository = try XCTUnwrap(client.authenticationRepository as? AuthenticationRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectUserResult = .failure(testError) - var receivedError: Error? - client.connectUser(userInfo: userInfo, token: .unique()) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectUser(userInfo: userInfo, token: .unique(), completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectTokenProvider, on: authenticationRepository) XCTAssertEqual(receivedError, testError) @@ -589,15 +557,11 @@ final class ChatClient_Tests: XCTestCase { let client = ChatClient(config: inMemoryStorageConfig, environment: testEnv.environment) let userInfo = UserInfo(id: "id") let authenticationRepository = try XCTUnwrap(client.authenticationRepository as? AuthenticationRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectUserResult = .success(()) - var receivedError: Error? - client.connectUser(userInfo: userInfo, token: .unique()) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectUser(userInfo: userInfo, token: .unique(), completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectTokenProvider, on: authenticationRepository) XCTAssertNil(receivedError) @@ -607,16 +571,12 @@ final class ChatClient_Tests: XCTestCase { let client = ChatClient(config: inMemoryStorageConfig, environment: testEnv.environment) let userInfo = UserInfo(id: "id") let authenticationRepository = try XCTUnwrap(client.authenticationRepository as? AuthenticationRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectUserResult = .success(()) - var receivedError: Error? let expiringToken = Token(rawValue: "", userId: "123", expiration: Date()) - client.connectUser(userInfo: userInfo, token: expiringToken) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectUser(userInfo: userInfo, token: expiringToken, completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertNotCall(AuthenticationRepository_Mock.Signature.connectTokenProvider, on: authenticationRepository) XCTAssertTrue(receivedError is ClientError.MissingTokenProvider) @@ -626,16 +586,12 @@ final class ChatClient_Tests: XCTestCase { let client = ChatClient(config: inMemoryStorageConfig, environment: testEnv.environment) let userInfo = UserInfo(id: "id") let authenticationRepository = try XCTUnwrap(client.authenticationRepository as? AuthenticationRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectUserResult = .success(()) - var receivedError: Error? let token = Token.development(userId: .unique) - client.connectUser(userInfo: userInfo, token: token) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectUser(userInfo: userInfo, token: token, completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectTokenProvider, on: authenticationRepository) XCTAssertNil(receivedError) @@ -645,17 +601,13 @@ final class ChatClient_Tests: XCTestCase { let client = ChatClient(config: inMemoryStorageConfig, environment: testEnv.environment) let userInfo = UserInfo(id: "id") let authenticationRepository = try XCTUnwrap(client.authenticationRepository as? AuthenticationRepository_Mock) - let expectation = self.expectation(description: "Connect completes") let mockedError = TestError() authenticationRepository.connectUserResult = .failure(mockedError) - var receivedError: Error? let token = Token.development(userId: .unique) - client.connectUser(userInfo: userInfo, token: token) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectUser(userInfo: userInfo, token: token, completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectTokenProvider, on: authenticationRepository) XCTAssertEqual(receivedError, mockedError) @@ -684,15 +636,11 @@ final class ChatClient_Tests: XCTestCase { let testError = TestError() let userInfo = UserInfo(id: "id") let authenticationRepository = try XCTUnwrap(client.authenticationRepository as? AuthenticationRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectGuestResult = .failure(testError) - var receivedError: Error? - client.connectGuestUser(userInfo: userInfo) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectGuestUser(userInfo: userInfo, completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectGuest, on: authenticationRepository) XCTAssertEqual(receivedError, testError) @@ -705,15 +653,11 @@ final class ChatClient_Tests: XCTestCase { let reconnectionTimeoutHandler = try XCTUnwrap(client.reconnectionTimeoutHandler as? ScheduledStreamTimer_Mock) let connectionRecoveryHandler = try XCTUnwrap(client.connectionRecoveryHandler as? ConnectionRecoveryHandler_Mock) let connectionRepository = try XCTUnwrap(client.connectionRepository as? ConnectionRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectGuestResult = .success(()) - var receivedError: Error? - client.connectGuestUser(userInfo: userInfo) { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectGuestUser(userInfo: userInfo, completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertNil(receivedError) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectGuest, on: authenticationRepository) @@ -728,15 +672,11 @@ final class ChatClient_Tests: XCTestCase { let client = ChatClient(config: inMemoryStorageConfig, environment: testEnv.environment) let testError = TestError() let authenticationRepository = try XCTUnwrap(client.authenticationRepository as? AuthenticationRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectAnonResult = .failure(testError) - var receivedError: Error? - client.connectAnonymousUser { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectAnonymousUser(completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectAnon, on: authenticationRepository) XCTAssertEqual(receivedError, testError) @@ -748,15 +688,11 @@ final class ChatClient_Tests: XCTestCase { let reconnectionTimeoutHandler = try XCTUnwrap(client.reconnectionTimeoutHandler as? ScheduledStreamTimer_Mock) let connectionRecoveryHandler = try XCTUnwrap(client.connectionRecoveryHandler as? ConnectionRecoveryHandler_Mock) let connectionRepository = try XCTUnwrap(client.connectionRepository as? ConnectionRepository_Mock) - let expectation = self.expectation(description: "Connect completes") authenticationRepository.connectAnonResult = .success(()) - var receivedError: Error? - client.connectAnonymousUser { - receivedError = $0 - expectation.fulfill() + let receivedError = try waitFor { done in + client.connectAnonymousUser(completion: done) } - waitForExpectations(timeout: defaultTimeout) XCTAssertNil(receivedError) XCTAssertCall(AuthenticationRepository_Mock.Signature.connectAnon, on: authenticationRepository) @@ -916,21 +852,17 @@ final class ChatClient_Tests: XCTestCase { XCTAssertNil(client.webSocketClient) } - func test_passiveClient_provideConnectionId_returnsImmediately() { + func test_passiveClient_provideConnectionId_returnsImmediately() throws { // Create Client with inactive flag set let client = ChatClient(config: inactiveInMemoryStorageConfig) // Set a connection Id waiter - var providedConnectionId: ConnectionId? = .unique - var connectionIdCallbackCalled = false - client.provideConnectionId { - providedConnectionId = $0.value - connectionIdCallbackCalled = true + let result = try waitFor { done in + client.provideConnectionId(completion: done) } - AssertAsync.willBeTrue(connectionIdCallbackCalled) // Assert that `nil` id is provided by waiter - XCTAssertNil(providedConnectionId) + XCTAssertNil(result.value) } func test_sessionHeaders_xStreamClient_correctValue() { @@ -1044,20 +976,6 @@ final class ChatClient_Tests: XCTestCase { } } -final class TestWorker: Worker { - let id = UUID() - - var init_database: DatabaseContainer? - var init_apiClient: APIClient? - - override init(database: DatabaseContainer, apiClient: APIClient) { - init_database = database - init_apiClient = apiClient - - super.init(database: database, apiClient: apiClient) - } -} - /// A helper class which provides mock environment for Client. private class TestEnvironment { @Atomic var apiClient: APIClient_Spy? diff --git a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController+Combine_Tests.swift index d12dfe7e3fa..8573a519923 100644 --- a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController+Combine_Tests.swift @@ -40,7 +40,7 @@ final class ChannelController_Combine_Tests: iOS13TestCase { weak var controller: ChannelControllerSpy? = channelController channelController = nil - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [weak controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } XCTAssertEqual(recording.output, [.localDataFetched, .remoteDataFetched]) } @@ -61,7 +61,7 @@ final class ChannelController_Combine_Tests: iOS13TestCase { let newChannel: ChatChannel = .mock(cid: .unique, name: .unique, imageURL: .unique(), extraData: [:]) controller?.channel_simulated = newChannel - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.channelController(controller!, didUpdateChannel: .create(newChannel)) } @@ -84,7 +84,7 @@ final class ChannelController_Combine_Tests: iOS13TestCase { let newMessage: ChatMessage = .unique controller?.messages_simulated = [newMessage] - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.channelController(controller!, didUpdateMessages: [.insert(newMessage, index: .init())]) } @@ -106,7 +106,7 @@ final class ChannelController_Combine_Tests: iOS13TestCase { channelController = nil let memberEvent: TestMemberEvent = .unique - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.channelController(controller!, didReceiveMemberEvent: memberEvent) } @@ -145,7 +145,7 @@ final class ChannelController_Combine_Tests: iOS13TestCase { extraData: [:] ) - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.channelController(controller!, didChangeTypingUsers: [typingUser]) } diff --git a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift index f33319b52b9..2cef0ccb4b1 100644 --- a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift @@ -608,7 +608,7 @@ final class ChannelController_Tests: XCTestCase { func test_synchronize_callsChannelUpdater() throws { // Simulate `synchronize` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.synchronize { [callbackQueueID] error in XCTAssertNil(error) AssertTestQueue(withId: callbackQueueID) @@ -753,7 +753,7 @@ final class ChannelController_Tests: XCTestCase { func test_synchronize_propagesErrorFromUpdater() { // Simulate `synchronize` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.synchronize { [callbackQueueID] in completionCalledError = $0 AssertTestQueue(withId: callbackQueueID) @@ -933,7 +933,7 @@ final class ChannelController_Tests: XCTestCase { before: sortedMessages[0].createdAt, after: sortedMessages[1].createdAt ) - var oldMessageId: MessageId? + nonisolated(unsafe) var oldMessageId: MessageId? // Save the message payload and check `channel.lastMessageAt` is not updated by older message try client.databaseContainer.writeSynchronously { let dto = try $0.createNewMessage( @@ -1810,7 +1810,7 @@ final class ChannelController_Tests: XCTestCase { func test_updateChannel_callsChannelUpdater() { // Simulate `updateChannel` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.updateChannel(name: .unique, imageURL: .unique(), team: .unique, extraData: .init()) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -1840,7 +1840,7 @@ final class ChannelController_Tests: XCTestCase { func test_updateChannel_propagesErrorFromUpdater() { // Simulate `updateChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.updateChannel(name: .unique, imageURL: .unique(), team: .unique, extraData: .init()) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -1861,7 +1861,7 @@ final class ChannelController_Tests: XCTestCase { let query = ChannelQuery(channelPayload: .unique) setupControllerForNewChannel(query: query) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "partialChannelUpdate completes") controller.partialChannelUpdate { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) @@ -1882,7 +1882,7 @@ final class ChannelController_Tests: XCTestCase { let extraData: [String: RawJSON] = ["scope": "test"] let unsetProperties: [String] = ["user.id", "channel_store"] - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "partialChannelUpdate completes") controller.partialChannelUpdate( name: name, @@ -1917,7 +1917,7 @@ final class ChannelController_Tests: XCTestCase { } func test_partialChannelUpdate_propagatesError() throws { - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "partialChannelUpdate completes") controller.partialChannelUpdate( name: .unique, @@ -2009,7 +2009,7 @@ final class ChannelController_Tests: XCTestCase { func test_muteChannel_callsChannelUpdater() { // Simulate `muteChannel` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.muteChannel { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2043,7 +2043,7 @@ final class ChannelController_Tests: XCTestCase { let expiration = 1_000_000 // Simulate `muteChannel` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.muteChannel(expiration: expiration) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2076,7 +2076,7 @@ final class ChannelController_Tests: XCTestCase { func test_muteChannel_propagatesErrorFromUpdater() { // Simulate `muteChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.muteChannel { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2094,7 +2094,7 @@ final class ChannelController_Tests: XCTestCase { let expiration = 1_000_000 // Simulate `muteChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.muteChannel(expiration: expiration) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2141,7 +2141,7 @@ final class ChannelController_Tests: XCTestCase { func test_unmuteChannel_callsChannelUpdater() { // Simulate `unmuteChannel` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.unmuteChannel { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2173,7 +2173,7 @@ final class ChannelController_Tests: XCTestCase { func test_unmuteChannel_propagatesErrorFromUpdater() { // Simulate `unmuteChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.unmuteChannel { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2305,7 +2305,7 @@ final class ChannelController_Tests: XCTestCase { func test_deleteChannel_callsChannelUpdater() { // Simulate `deleteChannel` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.deleteChannel { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2336,7 +2336,7 @@ final class ChannelController_Tests: XCTestCase { func test_deleteChannel_callsChannelUpdaterWithError() { // Simulate `muteChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.deleteChannel { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2383,7 +2383,7 @@ final class ChannelController_Tests: XCTestCase { func test_truncateChannel_callsChannelUpdater() { // Simulate `truncateChannel` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.truncateChannel { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2414,7 +2414,7 @@ final class ChannelController_Tests: XCTestCase { func test_truncateChannel_callsChannelUpdaterWithError() { // Simulate `truncateChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.truncateChannel { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2461,7 +2461,7 @@ final class ChannelController_Tests: XCTestCase { func test_hideChannel_callsChannelUpdater() { // Simulate `hideChannel` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.hideChannel(clearHistory: false) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2493,7 +2493,7 @@ final class ChannelController_Tests: XCTestCase { func test_hideChannel_callsChannelUpdaterWithError() { // Simulate `hideChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.hideChannel(clearHistory: false) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2540,7 +2540,7 @@ final class ChannelController_Tests: XCTestCase { func test_showChannel_callsChannelUpdater() { // Simulate `showChannel` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.showChannel { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2571,7 +2571,7 @@ final class ChannelController_Tests: XCTestCase { func test_showChannel_callsChannelUpdaterWithError() { // Simulate `showChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.showChannel { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2591,7 +2591,7 @@ final class ChannelController_Tests: XCTestCase { let channel = try setupChannel(channelPayload: dummyPayload(with: channelId, numberOfMessages: 1)) let messageId = channel.messages.first?.id - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.loadPreviousMessages(before: messageId, limit: 25) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2672,7 +2672,7 @@ final class ChannelController_Tests: XCTestCase { try setupChannel(channelPayload: dummyPayload(with: channelId, numberOfMessages: 1)) // Simulate `loadPreviousMessages` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.loadPreviousMessages(before: .unique) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2701,7 +2701,7 @@ final class ChannelController_Tests: XCTestCase { waitForExpectations(timeout: defaultTimeout) let expectation2 = self.expectation(description: "loadPreviousMessage completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? controller.loadPreviousMessages(before: "3") { error in receivedError = error expectation2.fulfill() @@ -2726,7 +2726,7 @@ final class ChannelController_Tests: XCTestCase { env.channelUpdater?.mockPaginationState.oldestFetchedMessage = .dummy(messageId: lastFetchedId) let exp = expectation(description: "loadPreviousMessage completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? controller.loadPreviousMessages() { error in receivedError = error exp.fulfill() @@ -2771,7 +2771,7 @@ final class ChannelController_Tests: XCTestCase { } let error = TestError() - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation2 = expectation(description: "loadPreviousMessage completes") controller.loadPreviousMessages() { error in @@ -2807,7 +2807,7 @@ final class ChannelController_Tests: XCTestCase { messageId = channel.messages.first?.id - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.loadNextMessages(after: messageId, limit: 25) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2847,7 +2847,7 @@ final class ChannelController_Tests: XCTestCase { withAllNextMessagesLoaded: false ) - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? let exp = expectation(description: "load next messages completion called") controller.loadNextMessages(after: .unique) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) @@ -2900,7 +2900,7 @@ final class ChannelController_Tests: XCTestCase { try setupChannel(withAllNextMessagesLoaded: false) let exp = expectation(description: "loadNextMessage completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? controller.loadNextMessages(after: "3") { error in receivedError = error exp.fulfill() @@ -2927,7 +2927,7 @@ final class ChannelController_Tests: XCTestCase { env.channelUpdater?.mockPaginationState.newestFetchedMessage = .dummy(messageId: lastFetchedId) let exp = expectation(description: "loadNextMessage completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? controller.loadNextMessages() { error in receivedError = error exp.fulfill() @@ -2962,7 +2962,7 @@ final class ChannelController_Tests: XCTestCase { try session.saveChannel(payload: dummyChannel) } - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.loadPageAroundMessageId(messageId, limit: 5) { error in XCTAssertNil(error) completionCalled = true @@ -3089,7 +3089,7 @@ final class ChannelController_Tests: XCTestCase { env.eventSender!.keystroke_completion_expectation = XCTestExpectation() // Simulate `keystroke` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.sendKeystrokeEvent { completionCalledError = $0 } // Keep a weak ref so we can check if it's actually deallocated @@ -3124,7 +3124,7 @@ final class ChannelController_Tests: XCTestCase { let parentMessageId = MessageId.unique // Simulate `keystroke` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.sendKeystrokeEvent(parentMessageId: parentMessageId) { completionCalledError = $0 } // Keep a weak ref so we can check if it's actually deallocated @@ -3164,7 +3164,7 @@ final class ChannelController_Tests: XCTestCase { env.eventSender!.startTyping_completion_expectation = XCTestExpectation() // Simulate `startTyping` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.sendStartTypingEvent { completionCalledError = $0 } // Keep a weak ref so we can check if it's actually deallocated @@ -3199,7 +3199,7 @@ final class ChannelController_Tests: XCTestCase { let parentMessageId = MessageId.unique // Simulate `startTyping` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.sendStartTypingEvent(parentMessageId: parentMessageId) { completionCalledError = $0 } // Keep a weak ref so we can check if it's actually deallocated @@ -3239,7 +3239,7 @@ final class ChannelController_Tests: XCTestCase { env.eventSender!.stopTyping_completion_expectation = XCTestExpectation() // Simulate `stopTyping` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.sendStopTypingEvent { completionCalledError = $0 } // Keep a weak ref so we can check if it's actually deallocated @@ -3274,7 +3274,7 @@ final class ChannelController_Tests: XCTestCase { let parentMessageId = MessageId.unique // Simulate `stopTyping` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.sendStopTypingEvent(parentMessageId: parentMessageId) { completionCalledError = $0 } // Keep a weak ref so we can check if it's actually deallocated @@ -3307,7 +3307,7 @@ final class ChannelController_Tests: XCTestCase { try session.saveChannel(payload: payload) } - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let error: Error? = try waitFor { completion in controller.sendKeystrokeEvent { @@ -3326,7 +3326,7 @@ final class ChannelController_Tests: XCTestCase { try session.saveChannel(payload: payload) } - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let error: Error? = try waitFor { completion in controller.sendStartTypingEvent { @@ -3352,7 +3352,7 @@ final class ChannelController_Tests: XCTestCase { try session.saveChannel(payload: payload) } - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let error: Error? = try waitFor { completion in controller.sendStopTypingEvent { @@ -3457,7 +3457,7 @@ final class ChannelController_Tests: XCTestCase { let skipEnrichUrl = false // Simulate `createNewMessage` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.createNewMessage( text: text, pinning: pin, @@ -3552,7 +3552,7 @@ final class ChannelController_Tests: XCTestCase { } func test_createNewMessage_whenMessageTransformerIsProvided_callsChannelUpdaterWithTransformedValues() throws { - class MockTransformer: StreamModelsTransformer { + class MockTransformer: StreamModelsTransformer, @unchecked Sendable { var mockTransformedMessage = NewMessageTransformableInfo( text: "transformed", attachments: [.mockFile], @@ -3599,7 +3599,7 @@ final class ChannelController_Tests: XCTestCase { let text: String = .unique // Simulate `createNewMessage` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.createSystemMessage( text: text ) { [callbackQueueID] result in @@ -3655,7 +3655,7 @@ final class ChannelController_Tests: XCTestCase { let members: [MemberInfo] = [.init(userId: .unique, extraData: ["is_premium": true])] // Simulate `addMembers` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.addMembers(members) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -3689,7 +3689,7 @@ final class ChannelController_Tests: XCTestCase { let members: [MemberInfo] = [.init(userId: .unique, extraData: nil)] // Simulate `addMembers` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.addMembers(members) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -3709,7 +3709,7 @@ final class ChannelController_Tests: XCTestCase { let members: Set = [.unique, .unique] // Simulate `inviteMembers` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.inviteMembers(userIds: members) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -3743,7 +3743,7 @@ final class ChannelController_Tests: XCTestCase { let members: Set = [.unique, .unique] // Simulate `inviteMembers` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.inviteMembers(userIds: members) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -3762,7 +3762,7 @@ final class ChannelController_Tests: XCTestCase { func test_acceptInvite_callsChannelUpdater() { // Simulate `acceptInvite` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let message = "Hooray" controller.acceptInvite(message: message) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) @@ -3795,7 +3795,7 @@ final class ChannelController_Tests: XCTestCase { func test_acceptInvite_propagatesErrorFromUpdater() { // Simulate `inviteMembers` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.acceptInvite(message: "Hooray") { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -3814,7 +3814,7 @@ final class ChannelController_Tests: XCTestCase { func test_rejectInvite_callsChannelUpdater() { // Simulate `acceptInvite` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.rejectInvite { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -3845,7 +3845,7 @@ final class ChannelController_Tests: XCTestCase { func test_rejectInvite_propagatesErrorFromUpdater() { // Simulate `inviteMembers` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.rejectInvite { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -3896,7 +3896,7 @@ final class ChannelController_Tests: XCTestCase { let members: Set = [.unique] // Simulate `removeMembers` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.removeMembers(userIds: members) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -3930,7 +3930,7 @@ final class ChannelController_Tests: XCTestCase { let members: Set = [.unique] // Simulate `removeMembers` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.removeMembers(userIds: members) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -4022,7 +4022,7 @@ final class ChannelController_Tests: XCTestCase { client.setToken(token: .unique(userId: currentUser.id)) // WHEN - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.markRead { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -4058,7 +4058,7 @@ final class ChannelController_Tests: XCTestCase { } // WHEN - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.markRead { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -4101,7 +4101,7 @@ final class ChannelController_Tests: XCTestCase { client.setToken(token: .unique(userId: currentUser.id)) // WHEN - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.markRead { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -4147,7 +4147,7 @@ final class ChannelController_Tests: XCTestCase { client.setToken(token: .unique(userId: currentUser.id)) // WHEN - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.markRead { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -4192,7 +4192,7 @@ final class ChannelController_Tests: XCTestCase { client.setToken(token: .unique(userId: currentUser.id)) // WHEN - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.markRead { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -4221,7 +4221,7 @@ final class ChannelController_Tests: XCTestCase { // Simulate `markRead` call and catch the completion let expectation = XCTestExpectation(description: "Mark read") - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.markRead { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -4261,7 +4261,7 @@ final class ChannelController_Tests: XCTestCase { // MARK: - Mark unread func test_markUnread_whenChannelDoesNotExist() { - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "Mark Unread completes") controller.markUnread(from: .unique) { result in receivedError = result.error @@ -4282,7 +4282,7 @@ final class ChannelController_Tests: XCTestCase { try session.saveChannel(payload: channel) } - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "Mark Unread completes") controller.markUnread(from: .unique) { result in receivedError = result.error @@ -4337,7 +4337,7 @@ final class ChannelController_Tests: XCTestCase { client.setToken(token: .unique(userId: currentUserId)) try simulateMarkingAsRead(userId: currentUserId) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "Mark Unread completes") controller.markUnread(from: .unique) { result in receivedError = result.error @@ -4358,7 +4358,7 @@ final class ChannelController_Tests: XCTestCase { try session.saveChannel(payload: channel) } - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "Mark Unread completes") controller.markUnread(from: .unique) { result in receivedError = result.error @@ -4381,7 +4381,7 @@ final class ChannelController_Tests: XCTestCase { let mockedError = TestError() env.channelUpdater?.markUnread_completion_result = .failure(mockedError) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "Mark Unread completes") controller.markUnread(from: .unique) { result in receivedError = result.error @@ -4405,7 +4405,7 @@ final class ChannelController_Tests: XCTestCase { let updater = try XCTUnwrap(env.channelUpdater) updater.markUnread_completion_result = .success(ChatChannel.mock(cid: .unique)) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let messageId = MessageId.unique let expectation = self.expectation(description: "Mark Unread completes") controller.markUnread(from: messageId) { result in @@ -4441,7 +4441,7 @@ final class ChannelController_Tests: XCTestCase { let updater = try XCTUnwrap(env.channelUpdater) updater.markUnread_completion_result = .success(ChatChannel.mock(cid: .unique)) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "Mark Unread completes") controller.markUnread(from: messageId) { result in receivedError = result.error @@ -4579,7 +4579,7 @@ final class ChannelController_Tests: XCTestCase { func test_enableSlowMode_callsChannelUpdater() { // Simulate `enableSlowMode` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.enableSlowMode(cooldownDuration: .random(in: 1...120)) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -4610,7 +4610,7 @@ final class ChannelController_Tests: XCTestCase { func test_enableSlowMode_propagatesErrorFromUpdater() { // Simulate `enableSlowMode` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.enableSlowMode(cooldownDuration: .random(in: 1...120)) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -4657,7 +4657,7 @@ final class ChannelController_Tests: XCTestCase { func test_disableSlowMode_callsChannelUpdater() { // Simulate `disableSlowMode` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.disableSlowMode { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -4690,7 +4690,7 @@ final class ChannelController_Tests: XCTestCase { func test_disableSlowMode_propagatesErrorFromUpdater() { // Simulate `disableSlowMode` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.disableSlowMode { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -4804,7 +4804,7 @@ final class ChannelController_Tests: XCTestCase { func test_startWatching_callsChannelUpdater() { // Simulate `startWatching` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.startWatching(isInRecoveryMode: false) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -4854,7 +4854,7 @@ final class ChannelController_Tests: XCTestCase { func test_startWatching_propagatesErrorFromUpdater() { // Simulate `startWatching` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.startWatching(isInRecoveryMode: false) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -4927,7 +4927,7 @@ final class ChannelController_Tests: XCTestCase { env.channelUpdater?.cleanUp() - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "watchActiveChannel completion") controller.recoverWatchedChannel(recovery: true) { error in receivedError = error @@ -4973,7 +4973,7 @@ final class ChannelController_Tests: XCTestCase { func test_stopWatching_callsChannelUpdater() { // Simulate `stopWatching` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.stopWatching { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -5006,7 +5006,7 @@ final class ChannelController_Tests: XCTestCase { func test_stopWatching_propagatesErrorFromUpdater() { // Simulate `stopWatching` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.stopWatching { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -5069,7 +5069,7 @@ final class ChannelController_Tests: XCTestCase { func test_freezeChannel_callsChannelUpdater() { // Simulate `freezeChannel` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.freezeChannel { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -5105,7 +5105,7 @@ final class ChannelController_Tests: XCTestCase { func test_freezeChannel_propagatesErrorFromUpdater() { // Simulate `freezeChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.freezeChannel { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -5152,7 +5152,7 @@ final class ChannelController_Tests: XCTestCase { func test_unfreezeChannel_callsChannelUpdater() { // Simulate `unfreezeChannel` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.unfreezeChannel { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -5188,7 +5188,7 @@ final class ChannelController_Tests: XCTestCase { func test_unfreezeChannel_propagatesErrorFromUpdater() { // Simulate `freezeChannel` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.unfreezeChannel { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -5305,7 +5305,7 @@ final class ChannelController_Tests: XCTestCase { func test_uploadAttachment_callsChannelUpdater() { // Simulate `uploadFile` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.uploadAttachment(localFileURL: .localYodaImage, type: .image) { [callbackQueueID] result in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(result.error) @@ -5340,7 +5340,7 @@ final class ChannelController_Tests: XCTestCase { func test_uploadAttachment_propagatesErrorFromUpdater() { // Simulate `uploadFile` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.uploadAttachment(localFileURL: .localYodaImage, type: .image) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0.error @@ -5423,7 +5423,7 @@ final class ChannelController_Tests: XCTestCase { func test_loadPinnedMessages_propagatesErrorFromUpdater() { // Simulate `loadPinnedMessages` call and catch the completion - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.loadPinnedMessages { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0.error @@ -5651,7 +5651,7 @@ final class ChannelController_Tests: XCTestCase { let pollId = String.unique // Simulate `deletePoll` call and capture error - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let error: Error? = try waitFor { [callbackQueueID] completion in controller.deletePoll(pollId: pollId) { error in AssertTestQueue(withId: callbackQueueID) @@ -5674,7 +5674,7 @@ final class ChannelController_Tests: XCTestCase { let testError = TestError() // Simulate `deletePoll` call and capture error - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let error: Error? = try waitFor { [callbackQueueID] completion in controller.deletePoll(pollId: pollId) { error in AssertTestQueue(withId: callbackQueueID) @@ -5834,7 +5834,7 @@ extension ChannelController_Tests { waitForMessagesUpdate(count: count) {} } - private func writeAndWaitForMessageUpdates(count: Int, channelChanges: Bool = false, _ actions: @escaping (DatabaseSession) throws -> Void, file: StaticString = #file, line: UInt = #line) { + private func writeAndWaitForMessageUpdates(count: Int, channelChanges: Bool = false, _ actions: @escaping @Sendable(DatabaseSession) throws -> Void, file: StaticString = #file, line: UInt = #line) { waitForMessagesUpdate(count: count, channelChanges: channelChanges, file: file, line: line) { do { try client.databaseContainer.writeSynchronously(actions) diff --git a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController+Combine_Tests.swift index 5bef12aa3a9..53d34994643 100644 --- a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController+Combine_Tests.swift @@ -40,7 +40,7 @@ final class ChannelListController_Combine_Tests: iOS13TestCase { weak var controller: ChannelListController_Mock? = channelListController channelListController = nil - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } XCTAssertEqual(recording.output, [.localDataFetched, .remoteDataFetched]) } @@ -61,7 +61,7 @@ final class ChannelListController_Combine_Tests: iOS13TestCase { let newChannel: ChatChannel = .mock(cid: .unique, name: .unique, imageURL: .unique(), extraData: [:]) controller?.channels_simulated = [newChannel] - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeChannels: [.insert(newChannel, index: [0, 1])]) } diff --git a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift index 2c987e597e6..95bd54f9613 100644 --- a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift @@ -226,7 +226,7 @@ final class ChannelListController_Tests: XCTestCase { let queueId = UUID() controller.callbackQueue = .testQueue(withId: queueId) // Simulate `synchronize` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.synchronize { completionCalledError = $0 AssertTestQueue(withId: queueId) @@ -364,7 +364,7 @@ final class ChannelListController_Tests: XCTestCase { controller.synchronize() let channel = ChatChannel.mock(cid: .unique) - try? database.createChannel(cid: channel.cid, channelReads: []) + try? database.createChannel(cid: channel.cid) let event = makeChannelVisibleEvent(with: channel) let eventExpectation = XCTestExpectation(description: "Event processed") controller.client.eventNotificationCenter.process(event) { @@ -481,7 +481,7 @@ final class ChannelListController_Tests: XCTestCase { } func test_didReceiveEvent_whenFilterMatches_shouldLinkChannelToQuery() { - let filter: (ChatChannel) -> Bool = { channel in + let filter: @Sendable(ChatChannel) -> Bool = { channel in channel.memberCount == 4 } setupControllerWithFilter(filter) @@ -502,7 +502,7 @@ final class ChannelListController_Tests: XCTestCase { } func test_didReceiveEvent_whenFilterMatches_whenChannelAlreadyPresent_shouldNotLinkChannelToQuery() throws { - let filter: (ChatChannel) -> Bool = { channel in + let filter: @Sendable(ChatChannel) -> Bool = { channel in channel.memberCount == 4 } setupControllerWithFilter(filter) @@ -532,7 +532,7 @@ final class ChannelListController_Tests: XCTestCase { } func test_didReceiveEvent_whenFilterDoesNotMatch_shouldNotLinkChannelToQuery() { - let filter: (ChatChannel) -> Bool = { channel in + let filter: @Sendable(ChatChannel) -> Bool = { channel in channel.memberCount == 1 } setupControllerWithFilter(filter) @@ -549,7 +549,7 @@ final class ChannelListController_Tests: XCTestCase { } func test_didReceiveEvent_whenChannelUpdatedEvent_whenFilterDoesNotMatch_shouldUnlinkChannelFromQuery() throws { - let filter: (ChatChannel) -> Bool = { channel in + let filter: @Sendable(ChatChannel) -> Bool = { channel in channel.memberCount == 1 } setupControllerWithFilter(filter) @@ -578,7 +578,7 @@ final class ChannelListController_Tests: XCTestCase { } func test_didReceiveEvent_whenChannelUpdatedEvent__whenFilterMatches_shouldNotUnlinkChannelFromQuery() throws { - let filter: (ChatChannel) -> Bool = { channel in + let filter: @Sendable(ChatChannel) -> Bool = { channel in channel.memberCount == 4 } setupControllerWithFilter(filter) @@ -606,7 +606,7 @@ final class ChannelListController_Tests: XCTestCase { } func test_didReceiveEvent_whenChannelUpdatedEvent__whenFilterDoesNotMatch_whenChannelNotPresent_shouldNotUnlinkChannelFromQuery() throws { - let filter: (ChatChannel) -> Bool = { channel in + let filter: @Sendable(ChatChannel) -> Bool = { channel in channel.memberCount == 1 } setupControllerWithFilter(filter) @@ -728,7 +728,7 @@ final class ChannelListController_Tests: XCTestCase { // MARK: - Channels pagination func test_loadNextChannels_callsChannelListUpdater() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let limit = 42 controller.loadNextChannels(limit: limit) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) @@ -765,7 +765,7 @@ final class ChannelListController_Tests: XCTestCase { func test_loadNextChannels_callsChannelUpdaterWithError() { // Simulate `loadNextChannels` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.loadNextChannels { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -780,7 +780,7 @@ final class ChannelListController_Tests: XCTestCase { } func test_loadNextChannels_defaultPageSize_isCorrect() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let pageSize = Int.random(in: 1...42) query = .init(filter: .in(.members, values: [.unique]), pageSize: pageSize) @@ -831,7 +831,7 @@ final class ChannelListController_Tests: XCTestCase { env.channelListUpdater?.refreshLoadedChannelsResult = .success(Set(channels.map(\.cid))) let expectation = self.expectation(description: "Refresh loaded channels") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? controller.refreshLoadedChannels() { result in receivedError = result.error expectation.fulfill() @@ -852,7 +852,7 @@ final class ChannelListController_Tests: XCTestCase { env.channelListUpdater?.refreshLoadedChannelsResult = .failure(error) let expectation = self.expectation(description: "Reset Query completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? controller.refreshLoadedChannels { result in receivedError = result.error expectation.fulfill() @@ -866,7 +866,7 @@ final class ChannelListController_Tests: XCTestCase { func test_markAllRead_callsChannelListUpdater() { // Simulate `markRead` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.markAllRead { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -895,7 +895,7 @@ final class ChannelListController_Tests: XCTestCase { func test_markAllRead_propagatesErrorFromUpdater() { // Simulate `markRead` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.markAllRead { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -1871,7 +1871,7 @@ final class ChannelListController_Tests: XCTestCase { ) } - private func setupControllerWithFilter(_ filter: @escaping (ChatChannel) -> Bool) { + private func setupControllerWithFilter(_ filter: @escaping @Sendable(ChatChannel) -> Bool) { // Prepare controller controller = ChatChannelListController( query: query, @@ -1893,7 +1893,12 @@ final class ChannelListController_Tests: XCTestCase { waitForChannelsUpdate {} } - private func writeAndWaitForChannelsUpdates(_ actions: @escaping (DatabaseSession) throws -> Void, completion: ((Error?) -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { + private func writeAndWaitForChannelsUpdates( + _ actions: @escaping @Sendable(DatabaseSession) throws -> Void, + completion: (@Sendable(Error?) -> Void)? = nil, + file: StaticString = #file, + line: UInt = #line + ) { waitForChannelsUpdate(file: file, line: line) { if let completion = completion { client.databaseContainer.write(actions, completion: completion) diff --git a/Tests/StreamChatTests/Controllers/ChannelWatcherListController/ChatChannelWatcherListController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelWatcherListController/ChatChannelWatcherListController+Combine_Tests.swift index 4f7558ae8cd..04c9e268c82 100644 --- a/Tests/StreamChatTests/Controllers/ChannelWatcherListController/ChatChannelWatcherListController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelWatcherListController/ChatChannelWatcherListController+Combine_Tests.swift @@ -45,7 +45,7 @@ final class ChatChannelWatcherListController_Combine_Tests: iOS13TestCase { watcherListController = nil // Simulate delegate invocation. - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } // Assert all state changes are delivered. XCTAssertEqual(recording.output, [.localDataFetched, .remoteDataFetched]) @@ -67,7 +67,7 @@ final class ChatChannelWatcherListController_Combine_Tests: iOS13TestCase { // Simulate delegate invocation with the members change. let change: ListChange = .insert(.unique, index: [0, 1]) - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.channelWatcherListController(controller!, didChangeWatchers: [change]) } diff --git a/Tests/StreamChatTests/Controllers/ChannelWatcherListController/ChatChannelWatcherListController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelWatcherListController/ChatChannelWatcherListController_Tests.swift index a6476fb222d..fd7789cfb8f 100644 --- a/Tests/StreamChatTests/Controllers/ChannelWatcherListController/ChatChannelWatcherListController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelWatcherListController/ChatChannelWatcherListController_Tests.swift @@ -74,7 +74,7 @@ final class ChatChannelWatcherListController_Tests: XCTestCase { func test_synchronize_changesState_and_callsCompletionOnCallbackQueue() throws { // Simulate `synchronize` call. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.synchronize { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -110,7 +110,7 @@ final class ChatChannelWatcherListController_Tests: XCTestCase { env.watcherListObserverSynchronizeError = observerError // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -125,7 +125,7 @@ final class ChatChannelWatcherListController_Tests: XCTestCase { func test_synchronize_changesState_and_propagatesListUpdaterErrorOnCallbackQueue() { // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -283,10 +283,10 @@ final class ChatChannelWatcherListController_Tests: XCTestCase { try client.databaseContainer.createChannel(cid: query.cid) // Create 2 watchers - var watcher1: UserPayload = .dummy( + nonisolated(unsafe) var watcher1: UserPayload = .dummy( userId: watcher1ID ) - var watcher2: UserPayload = .dummy( + nonisolated(unsafe) var watcher2: UserPayload = .dummy( userId: watcher2ID ) @@ -408,7 +408,7 @@ final class ChatChannelWatcherListController_Tests: XCTestCase { func test_loadNextWatchers_propagatesError() { // Simulate `loadNextWatchers` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.loadNextWatchers { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -424,7 +424,7 @@ final class ChatChannelWatcherListController_Tests: XCTestCase { func test_loadNextWatchers_propagatesNilError() throws { // Simulate `loadNextWatchers` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.loadNextWatchers { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) diff --git a/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController+Combine_Tests.swift index 3ccee4eaffc..1f7012a204f 100644 --- a/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController+Combine_Tests.swift @@ -41,7 +41,7 @@ final class ChatConnectionController_Combine_Tests: iOS13TestCase { // Simulate connection status update let newStatus: ConnectionStatus = .connected - controller?.delegateCallback { $0.connectionController(controller!, didUpdateConnectionStatus: newStatus) } + controller?.delegateCallback { [controller] in $0.connectionController(controller!, didUpdateConnectionStatus: newStatus) } // Assert initial value as well as the update are received AssertAsync.willBeEqual(recording.output, [.initialized, newStatus]) diff --git a/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController+SwiftUI_Tests.swift b/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController+SwiftUI_Tests.swift index 67cbc7733a5..37cbdc818bb 100644 --- a/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController+SwiftUI_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController+SwiftUI_Tests.swift @@ -44,8 +44,8 @@ final class ChatConnectionController_SwiftUI_Tests: iOS13TestCase { } } -final class ChatConnectionControllerMock: ChatConnectionController { - var connectionStatus_simulated: ConnectionStatus? +final class ChatConnectionControllerMock: ChatConnectionController, @unchecked Sendable { + @Atomic var connectionStatus_simulated: ConnectionStatus? override var connectionStatus: ConnectionStatus { connectionStatus_simulated ?? super.connectionStatus } diff --git a/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController_Tests.swift b/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController_Tests.swift index a85eebb3f5f..2b6ea5f7526 100644 --- a/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ConnectionController/ConnectionController_Tests.swift @@ -103,7 +103,7 @@ final class ChatConnectionController_Tests: XCTestCase { connectionRepository.connectResult = error.map { .failure($0) } ?? .success(()) - var connectCompletionError: Error? + nonisolated(unsafe) var connectCompletionError: Error? let expectation = self.expectation(description: "Connect completes") controller.connect { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) diff --git a/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController+Combine_Tests.swift index c9bbb25cc8e..f455388d507 100644 --- a/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController+Combine_Tests.swift @@ -45,7 +45,7 @@ final class CurrentUserController_Combine_Tests: iOS13TestCase { let newCurrentUser: CurrentChatUser = .mock(id: .unique) controller?.currentUser_simulated = newCurrentUser - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.currentUserController(controller!, didChangeCurrentUser: .create(newCurrentUser)) } @@ -68,7 +68,7 @@ final class CurrentUserController_Combine_Tests: iOS13TestCase { let newUnreadCount: UnreadCount = .dummy controller?.unreadCount_simulated = newUnreadCount - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.currentUserController(controller!, didChangeCurrentUserUnreadCount: newUnreadCount) } diff --git a/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController_Tests.swift b/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController_Tests.swift index 6c886bab301..1ad7767a76e 100644 --- a/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController_Tests.swift @@ -93,7 +93,7 @@ final class CurrentUserController_Tests: XCTestCase { env.currentUserObserverStartUpdatingError = observerError // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -113,7 +113,7 @@ final class CurrentUserController_Tests: XCTestCase { // Simulate current user env.currentUserObserverItem = .mock(id: .unique) - var synchronizeCalled = false + nonisolated(unsafe) var synchronizeCalled = false controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -136,7 +136,7 @@ final class CurrentUserController_Tests: XCTestCase { // Simulate current user env.currentUserObserverItem = .mock(id: .unique) - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -222,7 +222,7 @@ final class CurrentUserController_Tests: XCTestCase { controller.synchronize() var extraData: [String: RawJSON] = [:] - var currentUserPayload: CurrentUserPayload = .dummy( + nonisolated(unsafe) var currentUserPayload: CurrentUserPayload = .dummy( userId: .unique, role: .user, extraData: extraData @@ -318,7 +318,7 @@ final class CurrentUserController_Tests: XCTestCase { // Simulate `connectUser` client.authenticationRepository.setMockToken() - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.updateUserData(name: .unique, imageURL: .unique(), userExtraData: [:]) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -336,7 +336,7 @@ final class CurrentUserController_Tests: XCTestCase { // Simulate `connectUser` client.authenticationRepository.setMockToken() - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller .updateUserData(name: .unique, imageURL: .unique(), userExtraData: [:]) { [callbackQueueID] error in // Assert callback queue is correct. @@ -405,7 +405,7 @@ final class CurrentUserController_Tests: XCTestCase { // Simulate `connectUser` client.authenticationRepository.setMockToken() - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.synchronizeDevices { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -421,7 +421,7 @@ final class CurrentUserController_Tests: XCTestCase { // Simulate `connectUser` client.authenticationRepository.setMockToken() - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.synchronizeDevices { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -497,7 +497,7 @@ final class CurrentUserController_Tests: XCTestCase { // Simulate `connectUser` client.authenticationRepository.setMockToken() - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.addDevice(.firebase(token: "test")) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -515,7 +515,7 @@ final class CurrentUserController_Tests: XCTestCase { // Simulate `connectUser` client.authenticationRepository.setMockToken() - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.addDevice(.firebase(token: "test")) { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -593,7 +593,7 @@ final class CurrentUserController_Tests: XCTestCase { let expectedId = String.unique - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.removeDevice(id: expectedId) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -613,7 +613,7 @@ final class CurrentUserController_Tests: XCTestCase { let expectedId = String.unique - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.removeDevice(id: expectedId) { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -679,7 +679,7 @@ final class CurrentUserController_Tests: XCTestCase { authenticationRepository.refreshTokenResult = error.map { .failure($0) } ?? .success(()) let expectation = self.expectation(description: "reloadCompletes") - var reloadUserIfNeededCompletionError: Error? + nonisolated(unsafe) var reloadUserIfNeededCompletionError: Error? controller.reloadUserIfNeeded { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) reloadUserIfNeededCompletionError = error @@ -697,7 +697,7 @@ final class CurrentUserController_Tests: XCTestCase { func test_markAllRead_callsChannelListUpdater() { // GIVEN - var completionCalled = false + nonisolated(unsafe) var completionCalled = false // WHEN controller.markAllRead { [callbackQueueID] error in @@ -727,7 +727,7 @@ final class CurrentUserController_Tests: XCTestCase { func test_markAllRead_propagatesErrorFromUpdater() { // GIVEN - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? let testError = TestError() // WHEN @@ -745,7 +745,7 @@ final class CurrentUserController_Tests: XCTestCase { // Delay execution for a bit to make sure background thread acquires lock // (from Atomic, in EntityDatabaseObserver.item) if we don't sleep, main thread acquires lock first // & no deadlock occurs - private func delayExecution(of function: @escaping (((Error?) -> Void)?) -> Void, onCompletion: (() -> Void)?) { + private func delayExecution(of function: @escaping @Sendable((@Sendable(Error?) -> Void)?) -> Void, onCompletion: (@Sendable() -> Void)?) { let exp = expectation(description: "completion called") DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { function() { _ in @@ -789,7 +789,7 @@ final class CurrentUserController_Tests: XCTestCase { client.authenticationRepository.setMockToken() // Call loadAllUnreads - var receivedUnreads: CurrentUserUnreads? + nonisolated(unsafe) var receivedUnreads: CurrentUserUnreads? let exp = expectation(description: "loadAllUnreads called") controller.loadAllUnreads { result in receivedUnreads = try? result.get() @@ -839,7 +839,7 @@ final class CurrentUserController_Tests: XCTestCase { client.authenticationRepository.setMockToken() // Call loadAllUnreads - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let exp = expectation(description: "loadAllUnreads called") controller.loadAllUnreads { [callbackQueueID] result in AssertTestQueue(withId: callbackQueueID) diff --git a/Tests/StreamChatTests/Controllers/EventsController/ChannelEventsController_Tests.swift b/Tests/StreamChatTests/Controllers/EventsController/ChannelEventsController_Tests.swift index 14e4f9f147e..b4c3a5a37e3 100644 --- a/Tests/StreamChatTests/Controllers/EventsController/ChannelEventsController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/EventsController/ChannelEventsController_Tests.swift @@ -148,7 +148,7 @@ final class ChannelEventsController_Tests: XCTestCase { controller.callbackQueue = callbackQueue // Simulate `sendEvent`. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.sendEvent(payload) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID!) completionError = $0 @@ -172,7 +172,7 @@ final class ChannelEventsController_Tests: XCTestCase { controller.callbackQueue = callbackQueue // Simulate `sendEvent` and catch the completion. - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.sendEvent(IdeaEventPayload.unique) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID!) XCTAssertNil(error) diff --git a/Tests/StreamChatTests/Controllers/EventsController/EventsController_Tests.swift b/Tests/StreamChatTests/Controllers/EventsController/EventsController_Tests.swift index cf827a1789e..9eeeb2d5712 100644 --- a/Tests/StreamChatTests/Controllers/EventsController/EventsController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/EventsController/EventsController_Tests.swift @@ -61,7 +61,7 @@ final class EventsController_Tests: XCTestCase { // MARK: - Event propagation func test_whenEventsNotificationIsObserved_onlyEventsThatShouldBeProcessed_areForwardedToDelegate() { - class EventsControllerMock: EventsController { + class EventsControllerMock: EventsController, @unchecked Sendable { lazy var shouldProcessEventMockFunc = MockFunc.mock(for: shouldProcessEvent) override func shouldProcessEvent(_ event: Event) -> Bool { diff --git a/Tests/StreamChatTests/Controllers/MemberController/MemberController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/MemberController/MemberController+Combine_Tests.swift index 3daf3215c32..091583f85b2 100644 --- a/Tests/StreamChatTests/Controllers/MemberController/MemberController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/MemberController/MemberController+Combine_Tests.swift @@ -46,7 +46,7 @@ final class MemberController_Combine_Tests: iOS13TestCase { memberController = nil // Simulate delegate invocation. - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } // Assert all state changes are delivered. XCTAssertEqual(recording.output, [.localDataFetched, .remoteDataFetched]) @@ -68,7 +68,7 @@ final class MemberController_Combine_Tests: iOS13TestCase { // Simulate delegate invocation with the new member. let newMember: ChatChannelMember = .dummy - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.memberController(controller!, didUpdateMember: .create(newMember)) } diff --git a/Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift b/Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift index bc4b167dc81..d0cb8886fd0 100644 --- a/Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift @@ -81,7 +81,7 @@ final class MemberController_Tests: XCTestCase { func test_synchronize_changesState_and_callsCompletionOnCallbackQueue() { // Simulate `synchronize` call. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.synchronize { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -117,7 +117,7 @@ final class MemberController_Tests: XCTestCase { env.memberObserverSynchronizeError = observerError // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -132,7 +132,7 @@ final class MemberController_Tests: XCTestCase { func test_synchronize_changesState_and_propagatesListUpdaterErrorOnCallbackQueue() { // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -308,7 +308,7 @@ final class MemberController_Tests: XCTestCase { func test_ban_propagatesError() { // Simulate `ban` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.ban { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -324,7 +324,7 @@ final class MemberController_Tests: XCTestCase { func test_ban_propagatesNilError() { // Simulate `ban` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.ban { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -370,7 +370,7 @@ final class MemberController_Tests: XCTestCase { func test_shadowBan_propagatesError() { // Simulate `shadowBan` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.shadowBan { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -386,7 +386,7 @@ final class MemberController_Tests: XCTestCase { func test_shadowBan_propagatesNilError() { // Simulate `shadowBan` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.shadowBan { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -432,7 +432,7 @@ final class MemberController_Tests: XCTestCase { func test_unban_propagatesError() { // Simulate `unban` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.unban { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -448,7 +448,7 @@ final class MemberController_Tests: XCTestCase { func test_unban_propagatesNilError() { // Simulate `unban` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.unban { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -490,7 +490,7 @@ final class MemberController_Tests: XCTestCase { let expectedError = TestError() // Simulate `partialUpdate` call and catch the completion - var receivedResult: Result? + nonisolated(unsafe) var receivedResult: Result? controller.partialUpdate(extraData: ["key": .string("value")], unsetProperties: ["field"]) { [callbackQueueID] result in AssertTestQueue(withId: callbackQueueID) receivedResult = result @@ -507,7 +507,7 @@ final class MemberController_Tests: XCTestCase { let expectedMember: ChatChannelMember = .mock(id: .unique) // Simulate `partialUpdate` call and catch the completion - var receivedResult: Result? + nonisolated(unsafe) var receivedResult: Result? controller.partialUpdate(extraData: ["key": .string("value")], unsetProperties: ["field"]) { [callbackQueueID] result in AssertTestQueue(withId: callbackQueueID) receivedResult = result diff --git a/Tests/StreamChatTests/Controllers/MemberListController/MemberListController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/MemberListController/MemberListController+Combine_Tests.swift index 7c6e5068cfb..2d5fb81bb0f 100644 --- a/Tests/StreamChatTests/Controllers/MemberListController/MemberListController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/MemberListController/MemberListController+Combine_Tests.swift @@ -45,7 +45,7 @@ final class MemberListController_Combine_Tests: iOS13TestCase { memberListController = nil // Simulate delegate invocation. - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } // Assert all state changes are delivered. XCTAssertEqual(recording.output, [.localDataFetched, .remoteDataFetched]) @@ -67,7 +67,7 @@ final class MemberListController_Combine_Tests: iOS13TestCase { // Simulate delegate invocation with the members change. let change: ListChange = .insert(.dummy, index: [0, 1]) - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.memberListController(controller!, didChangeMembers: [change]) } diff --git a/Tests/StreamChatTests/Controllers/MemberListController/MemberListController_Tests.swift b/Tests/StreamChatTests/Controllers/MemberListController/MemberListController_Tests.swift index 82ee47a2f92..b30a2efed27 100644 --- a/Tests/StreamChatTests/Controllers/MemberListController/MemberListController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/MemberListController/MemberListController_Tests.swift @@ -77,7 +77,7 @@ final class MemberListController_Tests: XCTestCase { func test_synchronize_changesState_and_callsCompletionOnCallbackQueue() { // Simulate `synchronize` call. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.synchronize { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -113,7 +113,7 @@ final class MemberListController_Tests: XCTestCase { env.memberListObserverSynchronizeError = observerError // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -128,7 +128,7 @@ final class MemberListController_Tests: XCTestCase { func test_synchronize_changesState_and_propagatesListUpdaterErrorOnCallbackQueue() { // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -286,11 +286,11 @@ final class MemberListController_Tests: XCTestCase { try client.databaseContainer.createChannel(cid: query.cid) // Create 2 members, the first created more recently - var member1: MemberPayload = .dummy( + nonisolated(unsafe) var member1: MemberPayload = .dummy( user: .dummy(userId: member1ID), createdAt: Date() ) - var member2: MemberPayload = .dummy( + nonisolated(unsafe) var member2: MemberPayload = .dummy( user: .dummy(userId: member2ID), createdAt: Date().addingTimeInterval(-10) ) @@ -378,7 +378,7 @@ final class MemberListController_Tests: XCTestCase { func test_loadNextMembers_propagatesError() { // Simulate `loadNextMembers` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.loadNextMembers { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -394,7 +394,7 @@ final class MemberListController_Tests: XCTestCase { func test_loadNextMembers_propagatesNilError() { // Simulate `loadNextMembers` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.loadNextMembers { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -403,13 +403,6 @@ final class MemberListController_Tests: XCTestCase { completionIsCalled = true } - // Keep a weak ref so we can check if it's actually deallocated - weak var weakController = controller - - // (Try to) deallocate the controller - // by not keeping any references to it - controller = nil - // Simulate successful network response. env.memberListUpdater!.load_completion!(.success([])) // Release reference of completion so we can deallocate stuff @@ -417,6 +410,14 @@ final class MemberListController_Tests: XCTestCase { // Assert completion is called. AssertAsync.willBeTrue(completionIsCalled) + + // Keep a weak ref so we can check if it's actually deallocated + weak var weakController = controller + + // (Try to) deallocate the controller + // by not keeping any references to it + controller = nil + // `weakController` should be deallocated too AssertAsync.canBeReleased(&weakController) } diff --git a/Tests/StreamChatTests/Controllers/MessageController/MessageController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/MessageController/MessageController+Combine_Tests.swift index db9ee47bfe0..943ff090937 100644 --- a/Tests/StreamChatTests/Controllers/MessageController/MessageController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/MessageController/MessageController+Combine_Tests.swift @@ -40,7 +40,7 @@ final class MessageController_Combine_Tests: iOS13TestCase { weak var controller: ChatMessageController_Mock? = messageController messageController = nil - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } AssertAsync.willBeEqual(recording.output, [.localDataFetched, .remoteDataFetched]) } @@ -61,7 +61,7 @@ final class MessageController_Combine_Tests: iOS13TestCase { let newMessage: ChatMessage = .unique controller?.message_mock = newMessage - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.messageController(controller!, didChangeMessage: .create(newMessage)) } @@ -84,7 +84,7 @@ final class MessageController_Combine_Tests: iOS13TestCase { let newReply: ChatMessage = .unique controller?.replies_mock = [newReply] - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.messageController(controller!, didChangeReplies: [.insert(newReply, index: .init())]) } @@ -115,7 +115,7 @@ final class MessageController_Combine_Tests: iOS13TestCase { extraData: [:] ) controller?.reactions = [] - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.messageController(controller!, didChangeReactions: [newReaction]) } diff --git a/Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift b/Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift index 6156818365c..9dede5d0515 100644 --- a/Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift @@ -279,7 +279,7 @@ final class MessageController_Tests: XCTestCase { func test_synchronize_forwardsUpdaterError() throws { // Simulate `synchronize` call - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.synchronize { completionError = $0 } @@ -298,8 +298,8 @@ final class MessageController_Tests: XCTestCase { func test_synchronize_changesStateCorrectly_ifNoErrorsHappen() throws { // Simulate `synchronize` call - var completionError: Error? - var completionCalled = false + nonisolated(unsafe) var completionError: Error? + nonisolated(unsafe) var completionCalled = false controller.synchronize { completionError = $0 completionCalled = true @@ -854,7 +854,7 @@ final class MessageController_Tests: XCTestCase { func test_deleteMessage_propagatesError() { // Simulate `deleteMessage` call and catch the completion - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.deleteMessage { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -870,7 +870,7 @@ final class MessageController_Tests: XCTestCase { func test_deleteMessage_propagatesNilError() { // Simulate `deleteMessage` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.deleteMessage { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) XCTAssertNil($0) @@ -917,7 +917,7 @@ final class MessageController_Tests: XCTestCase { func test_editMessage_propagatesError() { // Simulate `editMessage` call and catch the completion - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.editMessage(text: .unique) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -933,7 +933,7 @@ final class MessageController_Tests: XCTestCase { func test_editMessage_propagatesNilError() { // Simulate `editMessage` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.editMessage(text: .unique) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) XCTAssertNil($0) @@ -989,7 +989,7 @@ final class MessageController_Tests: XCTestCase { } func test_editMessage_whenMessageTransformerIsProvided_callsUpdaterWithTransformedValues() throws { - class MockTransformer: StreamModelsTransformer { + class MockTransformer: StreamModelsTransformer, @unchecked Sendable { var mockTransformedMessage = NewMessageTransformableInfo( text: "transformed", attachments: [.mockFile], @@ -1032,7 +1032,7 @@ final class MessageController_Tests: XCTestCase { func test_flag_propagatesError() { // Simulate `flag` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.flag { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -1048,7 +1048,7 @@ final class MessageController_Tests: XCTestCase { func test_flag_propagatesNilError() { // Simulate `flag` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.flag { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -1106,7 +1106,7 @@ final class MessageController_Tests: XCTestCase { func test_unflag_propagatesError() { // Simulate `unflag` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.unflag { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -1122,7 +1122,7 @@ final class MessageController_Tests: XCTestCase { func test_unflag_propagatesNilError() { // Simulate `unflag` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.unflag { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -1179,7 +1179,7 @@ final class MessageController_Tests: XCTestCase { let skipEnrichUrl = false // Simulate `createNewReply` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.createNewReply( text: text, pinning: pin, @@ -1253,7 +1253,7 @@ final class MessageController_Tests: XCTestCase { } func test_createNewReply_whenMessageTransformerIsProvided_callsUpdaterWithTransformedValues() throws { - class MockTransformer: StreamModelsTransformer { + class MockTransformer: StreamModelsTransformer, @unchecked Sendable { var mockTransformedMessage = NewMessageTransformableInfo( text: "transformed", attachments: [.mockFile], @@ -1296,7 +1296,7 @@ final class MessageController_Tests: XCTestCase { func test_loadPreviousReplies_propagatesError() { // Simulate `loadPreviousReplies` call and catch the completion - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.loadPreviousReplies { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -1312,7 +1312,7 @@ final class MessageController_Tests: XCTestCase { func test_loadPreviousReplies_propagatesNilError() { // Simulate `loadPreviousReplies` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.loadPreviousReplies { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) XCTAssertNil($0) @@ -1457,7 +1457,7 @@ final class MessageController_Tests: XCTestCase { replyPaginationHandler.mockState.hasLoadedAllNextMessages = false // Simulate `loadNextReplies` call and catch the completion - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.loadNextReplies(after: .unique) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -1477,7 +1477,7 @@ final class MessageController_Tests: XCTestCase { replyPaginationHandler.mockState.hasLoadedAllNextMessages = false // Simulate `loadNextReplies` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.loadNextReplies(after: .unique) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) XCTAssertNil($0) @@ -1634,7 +1634,7 @@ final class MessageController_Tests: XCTestCase { func test_loadFirstPage_whenError() throws { let exp = expectation(description: "load first page completes") - var expectedError: Error? + nonisolated(unsafe) var expectedError: Error? controller.loadFirstPage() { error in expectedError = error exp.fulfill() @@ -1682,7 +1682,7 @@ final class MessageController_Tests: XCTestCase { } func test_loadReactions_propagatesError() { - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.loadReactions(limit: 25) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0.error @@ -1697,7 +1697,7 @@ final class MessageController_Tests: XCTestCase { } func test_loadReactions_propagatesReactions() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.loadReactions(limit: 25) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) let reactions = try? $0.get() @@ -1917,7 +1917,7 @@ final class MessageController_Tests: XCTestCase { func test_addReaction_propagatesError() { // Simulate `addReaction` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.addReaction(.init(rawValue: .unique)) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -1933,7 +1933,7 @@ final class MessageController_Tests: XCTestCase { func test_addReaction_propagatesNilError() { // Simulate `addReaction` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.addReaction(.init(rawValue: .unique)) { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -1997,7 +1997,7 @@ final class MessageController_Tests: XCTestCase { func test_deleteReaction_propagatesError() { // Simulate `deleteReaction` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.deleteReaction(.init(rawValue: .unique)) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -2013,7 +2013,7 @@ final class MessageController_Tests: XCTestCase { func test_deleteReaction_propagatesNilError() { // Simulate `deleteReaction` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.deleteReaction(.init(rawValue: .unique)) { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -2070,7 +2070,7 @@ final class MessageController_Tests: XCTestCase { let pinning = MessagePinning(expirationDate: .unique) // Simulate `pin` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.pin(pinning) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2102,7 +2102,7 @@ final class MessageController_Tests: XCTestCase { func test_pinMessage_callsMessageUpdaterWithError() { // Simulate `pin` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.pin(.noExpiration) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2118,7 +2118,7 @@ final class MessageController_Tests: XCTestCase { func test_unpinMessage_callsMessageUpdater() throws { // Simulate `unpin` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.unpin { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2149,7 +2149,7 @@ final class MessageController_Tests: XCTestCase { func test_unpinMessage_callsMessageUpdaterWithError() { // Simulate `unpin` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.unpin { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2169,7 +2169,7 @@ final class MessageController_Tests: XCTestCase { let attachmentId: AttachmentId = .unique // Simulate `restartFailedAttachmentUploading` call and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.restartFailedAttachmentUploading(with: attachmentId) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) @@ -2200,7 +2200,7 @@ final class MessageController_Tests: XCTestCase { func test_restartFailedAttachmentUploading_propagatesErrorFromUpdater() { // Simulate `restartFailedAttachmentUploading` call and catch the error. - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.restartFailedAttachmentUploading(with: .unique) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 @@ -2218,7 +2218,7 @@ final class MessageController_Tests: XCTestCase { func test_resendMessage_propagatesError() { // Simulate `resend` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.resendMessage { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -2234,7 +2234,7 @@ final class MessageController_Tests: XCTestCase { func test_resend_propagatesNilError() { // Simulate `resend` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.resendMessage { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -2274,7 +2274,7 @@ final class MessageController_Tests: XCTestCase { func test_dispatchEphemeralMessageAction_propagatesError() { // Simulate `dispatchEphemeralMessageAction` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.dispatchEphemeralMessageAction(.unique) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -2290,7 +2290,7 @@ final class MessageController_Tests: XCTestCase { func test_dispatchEphemeralMessageAction_propagatesNilError() { // Simulate `dispatchEphemeralMessageAction` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.dispatchEphemeralMessageAction(.unique) { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -2334,7 +2334,7 @@ final class MessageController_Tests: XCTestCase { func test_translate_propagatesError() { // Simulate `translate` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.translate(to: .english) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -2350,7 +2350,7 @@ final class MessageController_Tests: XCTestCase { func test_translate_propagatesNilError() { // Simulate `transate` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.translate(to: .english) { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -2548,7 +2548,7 @@ final class MessageController_Tests: XCTestCase { @discardableResult private func saveReplies(with payloads: [MessagePayload], channelPayload: ChannelPayload? = nil) throws -> [MessageDTO] { - var replies: [MessageDTO] = [] + nonisolated(unsafe) var replies: [MessageDTO] = [] try client.databaseContainer.writeSynchronously { session in try session.saveChannel(payload: channelPayload ?? .dummy(channel: .dummy(cid: self.cid))) diff --git a/Tests/StreamChatTests/Controllers/PollsControllers/PollController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/PollsControllers/PollController+Combine_Tests.swift index 2ca8828c9dd..b1b38351f34 100644 --- a/Tests/StreamChatTests/Controllers/PollsControllers/PollController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/PollsControllers/PollController+Combine_Tests.swift @@ -46,7 +46,7 @@ final class PollController_Combine_Tests: iOS13TestCase { weak var controller: PollController? = pollController pollController = nil - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } AssertAsync.willBeEqual(recording.output, [.initialized, .remoteDataFetched]) } @@ -66,7 +66,7 @@ final class PollController_Combine_Tests: iOS13TestCase { pollController = nil let poll: Poll = .unique - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.pollController(controller!, didUpdatePoll: .create(poll)) } @@ -97,7 +97,7 @@ final class PollController_Combine_Tests: iOS13TestCase { answerText: nil, user: .unique ) - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.pollController( controller!, didUpdateCurrentUserVotes: [.insert(pollVote, index: IndexPath(row: 0, section: 0))] diff --git a/Tests/StreamChatTests/Controllers/PollsControllers/PollController_Tests.swift b/Tests/StreamChatTests/Controllers/PollsControllers/PollController_Tests.swift index 04841662fd4..22010a27dbd 100644 --- a/Tests/StreamChatTests/Controllers/PollsControllers/PollController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/PollsControllers/PollController_Tests.swift @@ -82,7 +82,7 @@ final class PollController_Tests: XCTestCase { func test_synchronize_forwardsUpdaterError() throws { // Simulate `synchronize` call - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.synchronize { completionError = $0 } @@ -103,8 +103,8 @@ final class PollController_Tests: XCTestCase { func test_synchronize_changesStateCorrectly_ifNoErrorsHappen() throws { // Simulate `synchronize` call let expectation = expectation(description: "syncrhonize") - var completionError: Error? - var completionCalled = false + nonisolated(unsafe) var completionError: Error? + nonisolated(unsafe) var completionCalled = false controller.synchronize { completionError = $0 completionCalled = true @@ -189,7 +189,7 @@ final class PollController_Tests: XCTestCase { func test_addVote_propagatesError() { // Simulate `castPollVote` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? let expectation = expectation(description: "casting-vote") controller.castPollVote(answerText: nil, optionId: "123") { completionError = $0 @@ -208,7 +208,7 @@ final class PollController_Tests: XCTestCase { func test_addVote_propagatesSuccess() { // Simulate `addVote` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false let expectation = expectation(description: "casting-vote") controller.castPollVote(answerText: nil, optionId: "123") { error in XCTAssertNil(error) @@ -232,7 +232,7 @@ final class PollController_Tests: XCTestCase { func test_removeVote_propagatesError() { // Simulate `removePollVote` call and catch the completion. let expectation = expectation(description: "casting-vote") - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.removePollVote(voteId: "123") { completionError = $0 expectation.fulfill() @@ -250,7 +250,7 @@ final class PollController_Tests: XCTestCase { func test_removePollVote_propagatesSuccess() { // Simulate `removePollVote` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false let expectation = expectation(description: "remove-vote") controller.removePollVote(voteId: "123") { XCTAssertNil($0) @@ -274,7 +274,7 @@ final class PollController_Tests: XCTestCase { func test_closePoll_propagatesError() { // Simulate `closePoll` call and catch the completion. let expectation = expectation(description: "close-poll") - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.closePoll { completionError = $0 expectation.fulfill() @@ -293,7 +293,7 @@ final class PollController_Tests: XCTestCase { func test_closePoll_propagatesSuccess() { // Simulate `closePoll` call and catch the completion. let expectation = expectation(description: "close-poll") - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.closePoll { XCTAssertNil($0) completionIsCalled = true @@ -316,7 +316,7 @@ final class PollController_Tests: XCTestCase { func test_suggestPollOption_propagatesError() { // Simulate `suggestPollOption` call and catch the completion. let expectation = expectation(description: "poll-option") - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.suggestPollOption(text: "test") { completionError = $0 expectation.fulfill() @@ -334,7 +334,7 @@ final class PollController_Tests: XCTestCase { func test_suggestPollOption_propagatesSuccess() { // Simulate `suggestPollOption` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false let expectation = expectation(description: "poll-option") controller.suggestPollOption(text: "test") { XCTAssertNil($0) diff --git a/Tests/StreamChatTests/Controllers/PollsControllers/PollVoteListController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/PollsControllers/PollVoteListController+Combine_Tests.swift index 6810450646e..7e9d6ba8242 100644 --- a/Tests/StreamChatTests/Controllers/PollsControllers/PollVoteListController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/PollsControllers/PollVoteListController+Combine_Tests.swift @@ -52,7 +52,7 @@ final class PollVoteListController_Combine_Tests: iOS13TestCase { weak var controller: PollVoteListController? = voteListController voteListController = nil - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } AssertAsync.willBeEqual(recording.output, [.localDataFetched, .remoteDataFetched]) } @@ -73,7 +73,7 @@ final class PollVoteListController_Combine_Tests: iOS13TestCase { let vote: PollVote = .unique let changes: [ListChange] = .init([.insert(vote, index: .init())]) - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeVotes: changes) } diff --git a/Tests/StreamChatTests/Controllers/PollsControllers/PollVoteListController_Tests.swift b/Tests/StreamChatTests/Controllers/PollsControllers/PollVoteListController_Tests.swift index ba45a32507d..d5184c3f1e3 100644 --- a/Tests/StreamChatTests/Controllers/PollsControllers/PollVoteListController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/PollsControllers/PollVoteListController_Tests.swift @@ -84,7 +84,7 @@ final class PollVoteListController_Tests: XCTestCase { func test_synchronize_forwardsUpdaterError() throws { // Simulate `synchronize` call - var completionError: Error? + nonisolated(unsafe) var completionError: Error? let expectation = expectation(description: "synchronize") controller.synchronize { completionError = $0 @@ -104,8 +104,8 @@ final class PollVoteListController_Tests: XCTestCase { func test_synchronize_changesStateCorrectly_ifNoErrorsHappen() throws { // Simulate `synchronize` call - var completionError: Error? - var completionCalled = false + nonisolated(unsafe) var completionError: Error? + nonisolated(unsafe) var completionCalled = false let expectation = expectation(description: "synchronize") controller.synchronize { completionError = $0 diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/MessageSearchController/MessageSearchController_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/MessageSearchController/MessageSearchController_Tests.swift index 918f5d76d96..982ccb2fdcb 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/MessageSearchController/MessageSearchController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/MessageSearchController/MessageSearchController_Tests.swift @@ -109,7 +109,7 @@ final class MessageSearchController_Tests: XCTestCase { controller.callbackQueue = .testQueue(withId: queueId) // Simulate `search` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.search(text: "test") { error in XCTAssertNil(error) AssertTestQueue(withId: queueId) @@ -278,7 +278,7 @@ final class MessageSearchController_Tests: XCTestCase { let testError = TestError() // Make a search - var reportedError: Error? + nonisolated(unsafe) var reportedError: Error? controller.search(text: "test") { error in reportedError = error } @@ -320,7 +320,7 @@ final class MessageSearchController_Tests: XCTestCase { controller.callbackQueue = .testQueue(withId: queueId) // Simulate `search` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.search(query: query) { error in XCTAssertNil(error) AssertTestQueue(withId: queueId) @@ -529,7 +529,7 @@ final class MessageSearchController_Tests: XCTestCase { let testError = TestError() // Make a search - var reportedError: Error? + nonisolated(unsafe) var reportedError: Error? controller.search(query: query) { error in reportedError = error } @@ -544,7 +544,7 @@ final class MessageSearchController_Tests: XCTestCase { func test_loadNextMessages_propagatesError() { let testError = TestError() - var reportedError: Error? + nonisolated(unsafe) var reportedError: Error? // Make a search so we can call `loadNextMessages` controller.search(text: "test") @@ -649,7 +649,7 @@ final class MessageSearchController_Tests: XCTestCase { } func test_loadNextMessages_nextResultsPage_cantBeCalledBeforeSearch() { - var reportedError: Error? + nonisolated(unsafe) var reportedError: Error? controller.loadNextMessages { error in reportedError = error } diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController+Combine_Tests.swift index d61bf2bdf4e..93484dc53a4 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController+Combine_Tests.swift @@ -45,7 +45,7 @@ final class UserController_Combine_Tests: iOS13TestCase { userController = nil // Simulate delegate invocation. - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } // Assert all state changes are delivered. XCTAssertEqual(recording.output, [.localDataFetched, .remoteDataFetched]) @@ -67,7 +67,7 @@ final class UserController_Combine_Tests: iOS13TestCase { // Simulate delegate invocation with the new user. let newUser: ChatUser = .unique - controller?.delegateCallback { + controller?.delegateCallback { [controller] in $0.userController(controller!, didUpdateUser: .create(newUser)) } diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController_Tests.swift index 570632b74f2..a910e36d848 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController_Tests.swift @@ -72,7 +72,7 @@ final class UserController_Tests: XCTestCase { func test_synchronize_changesState_and_callsCompletionOnCallbackQueue() { // Simulate `synchronize` call. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.synchronize { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -109,7 +109,7 @@ final class UserController_Tests: XCTestCase { env.userObserverSynchronizeError = observerError // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -124,7 +124,7 @@ final class UserController_Tests: XCTestCase { func test_synchronize_changesState_and_propagatesUpdaterErrorOnCallbackQueue() { // Simulate `synchronize` call. - var synchronizeError: Error? + nonisolated(unsafe) var synchronizeError: Error? controller.synchronize { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) synchronizeError = error @@ -185,7 +185,7 @@ final class UserController_Tests: XCTestCase { func test_muteUser_propagatesError() { // Simulate `mute` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.mute { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -201,7 +201,7 @@ final class UserController_Tests: XCTestCase { func test_muteUser_propagatesNilError() { // Simulate `mute` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.mute { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -374,7 +374,7 @@ final class UserController_Tests: XCTestCase { func test_unmuteUser_propagatesError() { // Simulate `unmute` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.unmute { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -390,7 +390,7 @@ final class UserController_Tests: XCTestCase { func test_unmuteUser_propagatesNilError() { // Simulate `unmute` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.unmute { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -429,7 +429,7 @@ final class UserController_Tests: XCTestCase { func test_flagUser_propagatesError() { // Simulate `flag` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.flag { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -445,7 +445,7 @@ final class UserController_Tests: XCTestCase { func test_flagUser_propagatesNilError() { // Simulate `flag` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.flag { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -498,7 +498,7 @@ final class UserController_Tests: XCTestCase { func test_unflagUser_propagatesError() { // Simulate `unflag` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.unflag { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -514,7 +514,7 @@ final class UserController_Tests: XCTestCase { func test_unflagUser_propagatesNilError() { // Simulate `unflag` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.unflag { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -567,7 +567,7 @@ final class UserController_Tests: XCTestCase { func test_blockUser_propagatesError() { // Simulate `block` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.block { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -583,7 +583,7 @@ final class UserController_Tests: XCTestCase { func test_blockUser_propagatesNilError() { // Simulate `block` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.block { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) @@ -634,7 +634,7 @@ final class UserController_Tests: XCTestCase { func test_unblockUser_propagatesError() { // Simulate `unblock` call and catch the completion. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? controller.unblock { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionError = $0 @@ -650,7 +650,7 @@ final class UserController_Tests: XCTestCase { func test_unblockUser_propagatesNilError() { // Simulate `unblock` call and catch the completion. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false controller.unblock { [callbackQueueID] error in // Assert callback queue is correct. AssertTestQueue(withId: callbackQueueID) diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController+Combine_Tests.swift index 0e3bdcab254..6c607ff759a 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController+Combine_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController+Combine_Tests.swift @@ -40,7 +40,7 @@ final class UserListController_Combine_Tests: iOS13TestCase { weak var controller: UserListControllerMock? = userListController userListController = nil - controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) } + controller?.delegateCallback { [controller] in $0.controller(controller!, didChangeState: .remoteDataFetched) } XCTAssertEqual(recording.output, [.localDataFetched, .remoteDataFetched]) } @@ -61,7 +61,7 @@ final class UserListController_Combine_Tests: iOS13TestCase { let newUser: ChatUser = .unique controller?.users_simulated = [newUser] - controller?.delegateCallback { + controller?.delegateCallback { [weak controller] in $0.controller(controller!, didChangeUsers: [.insert(newUser, index: [0, 1])]) } diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController+SwiftUI_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController+SwiftUI_Tests.swift index 5be3fb911ee..90e729c0816 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController+SwiftUI_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController+SwiftUI_Tests.swift @@ -63,7 +63,7 @@ final class UserListController_SwiftUI_Tests: iOS13TestCase { } } -final class UserListControllerMock: ChatUserListController { +final class UserListControllerMock: ChatUserListController, @unchecked Sendable { @Atomic var synchronize_called = false var users_simulated: [ChatUser]? diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController_Tests.swift index 6b9e224ae4d..81293ae4f07 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/UserListController/UserListController_Tests.swift @@ -151,7 +151,7 @@ final class UserListController_Tests: XCTestCase { controller.callbackQueue = .testQueue(withId: queueId) // Simulate `synchronize` calls and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.synchronize { error in XCTAssertNil(error) AssertTestQueue(withId: queueId) @@ -185,7 +185,7 @@ final class UserListController_Tests: XCTestCase { let queueId = UUID() controller.callbackQueue = .testQueue(withId: queueId) // Simulate `synchronize` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.synchronize { completionCalledError = $0 AssertTestQueue(withId: queueId) @@ -273,7 +273,7 @@ final class UserListController_Tests: XCTestCase { // MARK: - Users pagination func test_loadNextUsers_callsUserListUpdater() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let limit = 42 controller.loadNextUsers(limit: limit) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) @@ -309,7 +309,7 @@ final class UserListController_Tests: XCTestCase { func test_loadNextUsers_callsUserUpdaterWithError() { // Simulate `loadNextUsers` call and catch the completion - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? controller.loadNextUsers { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/UserSearchController/UserSearchController_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/UserSearchController/UserSearchController_Tests.swift index 898fc6d6659..c61d10f39ff 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/UserSearchController/UserSearchController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/UserSearchController/UserSearchController_Tests.swift @@ -95,7 +95,7 @@ final class UserSearchController_Tests: XCTestCase { // Simulate `search` for 1st query and catch the completion let searchTerm1 = "1" - var searchCompletionCalled = false + nonisolated(unsafe) var searchCompletionCalled = false controller.search(term: searchTerm1) { error in XCTAssertNil(error) AssertTestQueue(withId: self.callbackQueueID) @@ -174,7 +174,7 @@ final class UserSearchController_Tests: XCTestCase { // Simulate `search` for 1st query and catch the completion let searchTerm1 = "1" - var search1CompletionCalled = false + nonisolated(unsafe) var search1CompletionCalled = false controller.search(term: searchTerm1) { error in XCTAssertNil(error) AssertTestQueue(withId: self.callbackQueueID) @@ -213,7 +213,7 @@ final class UserSearchController_Tests: XCTestCase { delegate.didChangeUsers_changes = nil // Simulate 2nd `search` calls and catch the completion - var search2CompletionError: Error? + nonisolated(unsafe) var search2CompletionError: Error? controller.search(term: .unique) { error in // Assert completion is called on callback queue AssertTestQueue(withId: self.callbackQueueID) @@ -239,7 +239,7 @@ final class UserSearchController_Tests: XCTestCase { func test_searchWithTerm_whenControllerHasInitialState_changesStateToLocalDataCached() { // Simulate `search` call and catch completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.search(term: .unique) { _ in completionCalled = true } @@ -294,7 +294,7 @@ final class UserSearchController_Tests: XCTestCase { controller.delegate = delegate // Simulate `search` for 1st query and catch the completion - var searchCompletionCalled = false + nonisolated(unsafe) var searchCompletionCalled = false controller.search(query: query) { error in XCTAssertNil(error) AssertTestQueue(withId: self.callbackQueueID) @@ -372,7 +372,7 @@ final class UserSearchController_Tests: XCTestCase { controller.delegate = delegate // Simulate `search` for 1st query and catch the completion - var search1CompletionCalled = false + nonisolated(unsafe) var search1CompletionCalled = false controller.search(query: query) { error in XCTAssertNil(error) AssertTestQueue(withId: self.callbackQueueID) @@ -411,7 +411,7 @@ final class UserSearchController_Tests: XCTestCase { delegate.didChangeUsers_changes = nil // Simulate 2nd `search` calls and catch the completion - var search2CompletionError: Error? + nonisolated(unsafe) var search2CompletionError: Error? controller.search(query: .user(withID: .unique)) { error in // Assert completion is called on callback queue AssertTestQueue(withId: self.callbackQueueID) @@ -437,7 +437,7 @@ final class UserSearchController_Tests: XCTestCase { func test_searchWithQuery_whenControllerHasInitialState_changesStateToLocalDataCached() { // Simulate `search` call and catch completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false controller.search(query: query) { _ in completionCalled = true } @@ -480,7 +480,7 @@ final class UserSearchController_Tests: XCTestCase { func test_loadNextUsers_whenCalledBeforeSearch_fails() { // Call `loadNextUsers` and catch the completion - var reportedError: Error? + nonisolated(unsafe) var reportedError: Error? controller.loadNextUsers { error in reportedError = error } @@ -494,7 +494,7 @@ final class UserSearchController_Tests: XCTestCase { func test_loadNextUsers_whenAPIRequestSucceeds() throws { // Simulate `search` for query and catch the completion - var searchCompletionCalled = false + nonisolated(unsafe) var searchCompletionCalled = false controller.search(query: query) { error in XCTAssertNil(error) AssertTestQueue(withId: self.callbackQueueID) @@ -518,7 +518,7 @@ final class UserSearchController_Tests: XCTestCase { // Simulate `loadNextUsers` and catch the completion let limit = 10 - var loadNextUsersCompletionCalled = false + nonisolated(unsafe) var loadNextUsersCompletionCalled = false controller.loadNextUsers(limit: limit) { error in XCTAssertNil(error) AssertTestQueue(withId: self.callbackQueueID) @@ -560,7 +560,7 @@ final class UserSearchController_Tests: XCTestCase { func test_loadNextUsers_whenAPIRequestFails() throws { // Simulate `search` for query and catch the completion - var searchCompletionCalled = false + nonisolated(unsafe) var searchCompletionCalled = false controller.search(query: query) { error in XCTAssertNil(error) AssertTestQueue(withId: self.callbackQueueID) @@ -587,7 +587,7 @@ final class UserSearchController_Tests: XCTestCase { controller.delegate = delegate // Simulate `loadNextUsers` and catch the completion - var loadNextUsersCompletionError: Error? + nonisolated(unsafe) var loadNextUsersCompletionError: Error? controller.loadNextUsers { error in AssertTestQueue(withId: self.callbackQueueID) loadNextUsersCompletionError = error @@ -612,7 +612,7 @@ final class UserSearchController_Tests: XCTestCase { func test_loadNextUsers_shouldNotKeepControllerAlive() throws { // Simulate `search` for query and catch the completion - var searchCompletionCalled = false + nonisolated(unsafe) var searchCompletionCalled = false controller.search(query: query) { _ in searchCompletionCalled = true } diff --git a/Tests/StreamChatTests/Database/DTOs/AttachmentDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/AttachmentDTO_Tests.swift index a708868443e..8a740567303 100644 --- a/Tests/StreamChatTests/Database/DTOs/AttachmentDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/AttachmentDTO_Tests.swift @@ -279,8 +279,8 @@ final class AttachmentDTO_Tests: XCTestCase { func test_attachmentChange_triggerMessageUpdate() throws { // Arrange: Store message with attachment in database - var messageId: MessageId! - var attachmentId: AttachmentId! + nonisolated(unsafe) var messageId: MessageId! + nonisolated(unsafe) var attachmentId: AttachmentId! let cid: ChannelId = .unique diff --git a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift index 9ffb8009d38..1da6a02c55b 100644 --- a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift @@ -206,7 +206,7 @@ final class ChannelDTO_Tests: XCTestCase { unreadMessagesCount: 0 ) - var channelPayload: ChannelPayload = .dummy( + nonisolated(unsafe) var channelPayload: ChannelPayload = .dummy( channel: .dummy(), messages: [message1], channelReads: [read1] @@ -1755,7 +1755,7 @@ private extension ChannelDTO_Tests { } } -private class CustomChannelTransformer: StreamModelsTransformer { +private class CustomChannelTransformer: StreamModelsTransformer, @unchecked Sendable { var mockTransformedChannel: ChatChannel = .mock(cid: .init(type: .messaging, id: "transformed")) func transform(channel: ChatChannel) -> ChatChannel { mockTransformedChannel diff --git a/Tests/StreamChatTests/Database/DTOs/MemberModelDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/MemberModelDTO_Tests.swift index 0a53b5edf76..7a793cd2fee 100644 --- a/Tests/StreamChatTests/Database/DTOs/MemberModelDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/MemberModelDTO_Tests.swift @@ -159,7 +159,7 @@ final class MemberModelDTO_Tests: XCTestCase { XCTAssertEqual(previousMembers.count, 4) // Save new members - var newMembers: [ChatChannelMember] = [] + nonisolated(unsafe) var newMembers: [ChatChannelMember] = [] try database.writeSynchronously { session in newMembers = try session.saveMembers(payload: members, channelId: cid, query: query) .map { try $0.asModel() } @@ -173,7 +173,7 @@ final class MemberModelDTO_Tests: XCTestCase { func test_saveMembers_whenAnotherPage_doesNotClearPreviousMembersFromQuery() throws { let cid: ChannelId = .unique let members: ChannelMemberListPayload = .init(members: [.dummy(), .dummy()]) - var query = ChannelMemberListQuery(cid: cid) + nonisolated(unsafe) var query = ChannelMemberListQuery(cid: cid) query.pagination = .init(pageSize: 20, offset: 25) // Save previous members @@ -181,7 +181,7 @@ final class MemberModelDTO_Tests: XCTestCase { XCTAssertEqual(previousMembers.count, 4) // Save new members - var newMembers: [ChatChannelMember] = [] + nonisolated(unsafe) var newMembers: [ChatChannelMember] = [] try database.writeSynchronously { session in newMembers = try session.saveMembers(payload: members, channelId: cid, query: query) .map { try $0.asModel() } @@ -194,7 +194,7 @@ final class MemberModelDTO_Tests: XCTestCase { } func test_asModel_whenModelTransformerProvided_transformsValues() throws { - class CustomMemberTransformer: StreamModelsTransformer { + class CustomMemberTransformer: StreamModelsTransformer, @unchecked Sendable { var mockTransformedMember: ChatChannelMember = .mock( id: .unique, name: "transformed member" diff --git a/Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift index e2fe560cad1..e85dbf34220 100644 --- a/Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift @@ -792,8 +792,8 @@ final class MessageDTO_Tests: XCTestCase { ) let (channelDTO, messageDTO): (ChannelDTO, MessageDTO) = try waitFor { completion in - var channelDTO: ChannelDTO! - var messageDTO: MessageDTO! + nonisolated(unsafe) var channelDTO: ChannelDTO! + nonisolated(unsafe) var messageDTO: MessageDTO! // Asynchronously save the payload to the db database.write { session in @@ -825,8 +825,8 @@ final class MessageDTO_Tests: XCTestCase { ) let (channelDTO, messageDTO): (ChannelDTO, MessageDTO) = try waitFor { completion in - var channelDTO: ChannelDTO! - var messageDTO: MessageDTO! + nonisolated(unsafe) var channelDTO: ChannelDTO! + nonisolated(unsafe) var messageDTO: MessageDTO! database.write { session in // Create the channel first @@ -893,8 +893,8 @@ final class MessageDTO_Tests: XCTestCase { ) let (channelDTO, messageDTO): (ChannelDTO, MessageDTO) = try waitFor { completion in - var channelDTO: ChannelDTO! - var messageDTO: MessageDTO! + nonisolated(unsafe) var channelDTO: ChannelDTO! + nonisolated(unsafe) var messageDTO: MessageDTO! // Asynchronously save the payload to the db database.write { session in @@ -926,8 +926,8 @@ final class MessageDTO_Tests: XCTestCase { reactionCounts: ["like": 2] ) let (_, _): (ChannelDTO, MessageDTO) = try waitFor { completion in - var channelDTO: ChannelDTO! - var messageDTO: MessageDTO! + nonisolated(unsafe) var channelDTO: ChannelDTO! + nonisolated(unsafe) var messageDTO: MessageDTO! // Asynchronously save the payload to the db database.write { session in @@ -1373,8 +1373,8 @@ final class MessageDTO_Tests: XCTestCase { // Create two messages in the DB - var message1Id: MessageId! - var message2Id: MessageId! + nonisolated(unsafe) var message1Id: MessageId! + nonisolated(unsafe) var message2Id: MessageId! _ = try waitFor { completion in database.write({ session in @@ -1532,7 +1532,7 @@ final class MessageDTO_Tests: XCTestCase { } // Create a new message - var newMessageId: MessageId! + nonisolated(unsafe) var newMessageId: MessageId! let newMessageText: String = .unique let newMessageCommand: String = .unique @@ -1616,7 +1616,7 @@ final class MessageDTO_Tests: XCTestCase { } // Create a new message - var newMessageId: MessageId! + nonisolated(unsafe) var newMessageId: MessageId! let newMessageText: String = .unique try database.writeSynchronously { session in let messageDTO = try session.createNewMessage( @@ -1668,7 +1668,7 @@ final class MessageDTO_Tests: XCTestCase { } // WHEN - var messageId: MessageId! + nonisolated(unsafe) var messageId: MessageId! try database.writeSynchronously { session in let messageDTO = try session.createNewMessage( in: cid, @@ -1716,7 +1716,7 @@ final class MessageDTO_Tests: XCTestCase { } // WHEN - var threadReplyId: MessageId! + nonisolated(unsafe) var threadReplyId: MessageId! try database.writeSynchronously { session in let replyShownInChannelDTO = try session.createNewMessage( in: cid, @@ -1898,7 +1898,7 @@ final class MessageDTO_Tests: XCTestCase { } // Create a new message - var newMessageId: MessageId! + nonisolated(unsafe) var newMessageId: MessageId! let newMessageText: String = .unique try database.writeSynchronously { session in @@ -1939,7 +1939,7 @@ final class MessageDTO_Tests: XCTestCase { let originalReplyCount = database.viewContext.message(id: messageId)?.replyCount ?? 0 // Reply messageId - var replyMessageId: MessageId? + nonisolated(unsafe) var replyMessageId: MessageId? // Create new reply message try database.writeSynchronously { session in @@ -4193,7 +4193,7 @@ final class MessageDTO_Tests: XCTestCase { } func test_asModel_whenModelTransformerProvided_transformsValues() throws { - class CustomMessageTransformer: StreamModelsTransformer { + class CustomMessageTransformer: StreamModelsTransformer, @unchecked Sendable { var mockTransformedMessage: ChatMessage = .mock( id: .unique, text: "transformed message" @@ -4232,7 +4232,7 @@ final class MessageDTO_Tests: XCTestCase { // MARK: - Helpers: private func message(with id: MessageId) -> ChatMessage? { - var message: ChatMessage? + nonisolated(unsafe) var message: ChatMessage? try? database.writeSynchronously { session in message = try session.message(id: id)?.asModel() } @@ -4240,7 +4240,7 @@ final class MessageDTO_Tests: XCTestCase { } private func reactionState(with messageId: String, userId: UserId, type: MessageReactionType) -> LocalReactionState? { - var reactionState: LocalReactionState? + nonisolated(unsafe) var reactionState: LocalReactionState? try? database.writeSynchronously { session in reactionState = session.reaction(messageId: messageId, userId: userId, type: type)?.localState } @@ -4316,7 +4316,7 @@ final class MessageDTO_Tests: XCTestCase { enforceUnique: Bool = false ) -> Result { do { - var reactionId: String! + nonisolated(unsafe) var reactionId: String! try database.writeSynchronously { database in let reaction = try database.addReaction( to: messageId, diff --git a/Tests/StreamChatTests/Database/DTOs/MessageReactionDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/MessageReactionDTO_Tests.swift index 982787a40da..f40581cf252 100644 --- a/Tests/StreamChatTests/Database/DTOs/MessageReactionDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/MessageReactionDTO_Tests.swift @@ -234,7 +234,7 @@ final class MessageReactionDTO_Tests: XCTestCase { messageId: MessageId, userId: UserId ) throws -> String { - var id: String! + nonisolated(unsafe) var id: String! try database.writeSynchronously { session in let reaction = try session.saveReaction( payload: MessageReactionPayload( diff --git a/Tests/StreamChatTests/Database/DTOs/NSManagedObject+Validation_Tests.swift b/Tests/StreamChatTests/Database/DTOs/NSManagedObject+Validation_Tests.swift index e0a900bb95d..38d8609b458 100644 --- a/Tests/StreamChatTests/Database/DTOs/NSManagedObject+Validation_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/NSManagedObject+Validation_Tests.swift @@ -33,10 +33,7 @@ final class NSManagedObject_Validation_Tests: XCTestCase { } func test_isValid_ReturnsFalse_WhenTheObjectIsDeleted() throws { - guard let message = try createMessage() else { - XCTFail() - return - } + nonisolated(unsafe) let message = try XCTUnwrap(createMessage()) try database.writeSynchronously { session in session.delete(message: message) @@ -48,10 +45,7 @@ final class NSManagedObject_Validation_Tests: XCTestCase { } func test_deletedObject_doesNotCrash_whenAccessingDate() throws { - guard let message = try createMessage() else { - XCTFail() - return - } + nonisolated(unsafe) let message = try XCTUnwrap(createMessage()) try database.writeSynchronously { session in session.delete(message: message) @@ -87,7 +81,7 @@ final class NSManagedObject_Validation_Tests: XCTestCase { private extension NSManagedObject_Validation_Tests { private func createMessage() throws -> MessageDTO? { let channelId = ChannelId(type: .messaging, id: "123") - var message: MessageDTO? + nonisolated(unsafe) var message: MessageDTO? try database.createCurrentUser() try database.createChannel(cid: channelId) try database.writeSynchronously { session in diff --git a/Tests/StreamChatTests/Database/DTOs/QueuedRequestDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/QueuedRequestDTO_Tests.swift index 49b82f428cc..951feac3447 100644 --- a/Tests/StreamChatTests/Database/DTOs/QueuedRequestDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/QueuedRequestDTO_Tests.swift @@ -95,7 +95,7 @@ final class QueuedRequestDTO_Tests: XCTestCase { } private func createRequests(count: Int) throws { - func createRequest(index: Int) throws { + @Sendable func createRequest(index: Int) throws { let id = "request\(index)" let date = Date() let endpoint = Endpoint( diff --git a/Tests/StreamChatTests/Database/DatabaseSession_Tests.swift b/Tests/StreamChatTests/Database/DatabaseSession_Tests.swift index 4e2854a7c50..eb06d006949 100644 --- a/Tests/StreamChatTests/Database/DatabaseSession_Tests.swift +++ b/Tests/StreamChatTests/Database/DatabaseSession_Tests.swift @@ -776,7 +776,7 @@ final class DatabaseSession_Tests: XCTestCase { // GIVEN let pollOptionId = "345" let pollId = "123" - var voteId: String! + nonisolated(unsafe) var voteId: String! let currentUserId = String.unique let payload = XCTestCase().dummyPollVotePayload(optionId: pollOptionId, pollId: pollId) @@ -812,7 +812,7 @@ final class DatabaseSession_Tests: XCTestCase { // GIVEN let pollOptionId = "345" let pollId = "123" - var voteId: String! + nonisolated(unsafe) var voteId: String! let currentUserId = String.unique let secondOptionId = "789" let firstOption = PollOptionPayload(id: pollOptionId, text: "First", custom: [:]) diff --git a/Tests/StreamChatTests/Query/Sorting/ListDatabaseObserver+Sorting_Tests.swift b/Tests/StreamChatTests/Query/Sorting/ListDatabaseObserver+Sorting_Tests.swift index c8cd44d1f74..181fde90fef 100644 --- a/Tests/StreamChatTests/Query/Sorting/ListDatabaseObserver+Sorting_Tests.swift +++ b/Tests/StreamChatTests/Query/Sorting/ListDatabaseObserver+Sorting_Tests.swift @@ -342,7 +342,7 @@ final class ListDatabaseObserver_Sorting_Tests: XCTestCase { @discardableResult private func createChannels(mapping: [(name: String, createdAt: Date, messageCreatedAt: Date)]) throws -> [ChannelId] { - var cids: [ChannelId] = [] + nonisolated(unsafe) var cids: [ChannelId] = [] try database.writeSynchronously { session in session.saveQuery(query: self.query) let channels = try mapping.map { (name, createdAt, messageCreatedAt) -> ChannelDTO in diff --git a/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift b/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift index 69ba5c3c67a..51e90965250 100644 --- a/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift @@ -144,7 +144,7 @@ final class AuthenticationRepository_Tests: XCTestCase { } func test_setToken_tokenIsUpdated_doesNotCallTokenWaiters_whenNotRequired() { - var completionExecuted = false + nonisolated(unsafe) var completionExecuted = false repository.provideToken { _ in completionExecuted = true } @@ -165,14 +165,14 @@ final class AuthenticationRepository_Tests: XCTestCase { // Token Provider Failure let testError = TestError() - var tokenCalls = 0 + nonisolated(unsafe) var tokenCalls = 0 let provider: TokenProvider = { tokenCalls += 1 $0(.failure(testError)) } let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? XCTAssertNil(repository.tokenProvider) repository.connectUser(userInfo: userInfo, tokenProvider: provider, completion: { error in @@ -195,7 +195,7 @@ final class AuthenticationRepository_Tests: XCTestCase { // Token Provider Failure let testError = TestError() let providedToken = Token.unique() - var tokenCalls = 0 + nonisolated(unsafe) var tokenCalls = 0 let provider: TokenProvider = { tokenCalls += 1 if tokenCalls > 8 { @@ -209,7 +209,7 @@ final class AuthenticationRepository_Tests: XCTestCase { connectionRepository.connectResult = .success(()) let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? XCTAssertNil(repository.tokenProvider) repository.connectUser(userInfo: userInfo, tokenProvider: provider, completion: { error in @@ -240,7 +240,7 @@ final class AuthenticationRepository_Tests: XCTestCase { connectionRepository.connectResult = .failure(testError) let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? XCTAssertNil(repository.tokenProvider) repository.connectUser(userInfo: userInfo, tokenProvider: provider, completion: { error in @@ -267,7 +267,7 @@ final class AuthenticationRepository_Tests: XCTestCase { connectionRepository.connectResult = .success(()) let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? XCTAssertNil(repository.tokenProvider) repository.connectUser(userInfo: userInfo, tokenProvider: provider, completion: { error in @@ -358,7 +358,7 @@ final class AuthenticationRepository_Tests: XCTestCase { connectionRepository.disconnectResult = .success(()) // First user - var initialCompletionCalls = 0 + nonisolated(unsafe) var initialCompletionCalls = 0 let userInfo = UserInfo(id: "123") let originalTokenProvider: TokenProvider = { $0(.success(.unique())) } let expectation1 = expectation(description: "Completion call 1") @@ -377,7 +377,7 @@ final class AuthenticationRepository_Tests: XCTestCase { // New token/user let newUserId = "user-id" let newUserInfo = UserInfo(id: newUserId) - var newTokenCompletionCalls = 0 + nonisolated(unsafe) var newTokenCompletionCalls = 0 let expectation2 = expectation(description: "Completion call 2") let newTokenProvider: TokenProvider = { $0(.success(.unique(userId: newUserId))) } repository.connectUser(userInfo: userInfo, tokenProvider: newTokenProvider, completion: { _ in @@ -394,7 +394,7 @@ final class AuthenticationRepository_Tests: XCTestCase { XCTAssertEqual(delegate.logoutCallCount, 1) // Refresh token - var refreshTokenCompletionCalls = 0 + nonisolated(unsafe) var refreshTokenCompletionCalls = 0 let expectation3 = expectation(description: "Completion call 2") let refreshTokenProvider: TokenProvider = { $0(.success(.unique(userId: newUserId))) } repository.connectUser(userInfo: newUserInfo, tokenProvider: refreshTokenProvider, completion: { _ in @@ -648,7 +648,7 @@ final class AuthenticationRepository_Tests: XCTestCase { apiClient.test_mockUnmanagedResponseResult(Result.failure(apiError)) let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? XCTAssertNil(repository.tokenProvider) repository.connectGuestUser(userInfo: userInfo, completion: { error in @@ -690,7 +690,7 @@ final class AuthenticationRepository_Tests: XCTestCase { ) let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? XCTAssertNil(repository.tokenProvider) repository.connectGuestUser(userInfo: userInfo, completion: { error in @@ -733,7 +733,7 @@ final class AuthenticationRepository_Tests: XCTestCase { ) let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? XCTAssertNil(repository.tokenProvider) repository.connectGuestUser(userInfo: userInfo, completion: { error in @@ -763,7 +763,7 @@ final class AuthenticationRepository_Tests: XCTestCase { connectionRepository.connectResult = .success(()) let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? XCTAssertNil(repository.tokenProvider) repository.connectAnonymousUser(completion: { error in @@ -831,7 +831,7 @@ final class AuthenticationRepository_Tests: XCTestCase { func test_refreshToken_returnsError_ifThereIsNoTokenProvider() { let expectation = self.expectation(description: "Refresh token") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? repository.refreshToken { error in receivedError = error expectation.fulfill() @@ -949,7 +949,7 @@ final class AuthenticationRepository_Tests: XCTestCase { let existingToken = Token.unique() repository.setToken(token: existingToken, completeTokenWaiters: false) - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Connection Id Completion") repository.provideToken(timeout: defaultTimeout) { result = $0 @@ -965,7 +965,7 @@ final class AuthenticationRepository_Tests: XCTestCase { } func test_provideToken_returnsErrorOnTimeout() { - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Token Completion") repository.provideToken(timeout: 0.01) { result = $0 @@ -979,7 +979,7 @@ final class AuthenticationRepository_Tests: XCTestCase { } func test_provideToken_returnsErrorOnMissingValue() { - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Token Completion") repository.provideToken(timeout: defaultTimeout) { result = $0 @@ -995,7 +995,7 @@ final class AuthenticationRepository_Tests: XCTestCase { } func test_provideToken_returnsValue_whenCompletingTokenWaiters() { - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Token Completion") repository.provideToken(timeout: defaultTimeout) { result = $0 @@ -1043,7 +1043,7 @@ final class AuthenticationRepository_Tests: XCTestCase { func test_reset() { let mockTimer = MockTimer() - FakeTimer.mockTimer = mockTimer + FakeTimer.mockTimer.value = mockTimer retryStrategy.consecutiveFailuresCount = 5 let repository = AuthenticationRepository( apiClient: apiClient, @@ -1084,7 +1084,7 @@ final class AuthenticationRepository_Tests: XCTestCase { let provider: TokenProvider = { $0(.success(newToken)) } let completionExpectation = expectation(description: "Connect completion") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? repository.connectUser(userInfo: newUserInfo, tokenProvider: provider, completion: { error in receivedError = error completionExpectation.fulfill() @@ -1104,7 +1104,7 @@ final class AuthenticationRepository_Tests: XCTestCase { connectionRepository.connectResult = mockedError.map { .failure($0) } ?? .success(()) let expectation = self.expectation(description: "Refresh token") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? repository.refreshToken { error in receivedError = error expectation.fulfill() @@ -1127,7 +1127,7 @@ final class AuthenticationRepository_Tests: XCTestCase { } } connectionRepository.connectResult = .success(()) - var isFirstTime = true + nonisolated(unsafe) var isFirstTime = true let expectation = self.expectation(description: "connect completes") repository.connectUser(userInfo: userInfo, tokenProvider: tokenProvider, completion: { _ in if isFirstTime { diff --git a/Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift b/Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift index 564a993ba45..4f677265d71 100644 --- a/Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/ChannelRepository_Tests.swift @@ -70,7 +70,7 @@ final class ChannelRepository_Tests: XCTestCase { let userId = UserId.unique let expectation = self.expectation(description: "markRead completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? repository.markRead(cid: cid, userId: userId) { error in receivedError = error expectation.fulfill() @@ -90,7 +90,7 @@ final class ChannelRepository_Tests: XCTestCase { let userId = UserId.unique let expectation = self.expectation(description: "markRead completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? repository.markRead(cid: cid, userId: userId) { error in receivedError = error expectation.fulfill() @@ -121,7 +121,7 @@ final class ChannelRepository_Tests: XCTestCase { database.writeSessionCounter = 0 let expectation = self.expectation(description: "markUnread completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? repository.markUnread(for: cid, userId: userId, from: messageId, lastReadMessageId: .unique) { result in receivedError = result.error expectation.fulfill() @@ -142,7 +142,7 @@ final class ChannelRepository_Tests: XCTestCase { let messageId = MessageId.unique let expectation = self.expectation(description: "markUnread completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? repository.markUnread(for: cid, userId: userId, from: messageId, lastReadMessageId: .unique) { result in receivedError = result.error expectation.fulfill() diff --git a/Tests/StreamChatTests/Repositories/ConnectionRepository_Tests.swift b/Tests/StreamChatTests/Repositories/ConnectionRepository_Tests.swift index d6ce536fc0d..e83d022b2ea 100644 --- a/Tests/StreamChatTests/Repositories/ConnectionRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/ConnectionRepository_Tests.swift @@ -51,7 +51,7 @@ final class ConnectionRepository_Tests: XCTestCase { timerType: DefaultTimer.self ) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "connect completes") repository.connect { receivedError = $0 @@ -67,7 +67,7 @@ final class ConnectionRepository_Tests: XCTestCase { repository.completeConnectionIdWaiters(connectionId: "123") XCTAssertNotNil(repository.connectionId) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "connect completes") repository.connect { receivedError = $0 @@ -83,7 +83,7 @@ final class ConnectionRepository_Tests: XCTestCase { func test_connect_noConnectionId_failure() throws { XCTAssertNil(repository.connectionId) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "connect completes") repository.connect { receivedError = $0 @@ -105,7 +105,7 @@ final class ConnectionRepository_Tests: XCTestCase { func test_connect_noConnectionId_invalidTokenError() throws { XCTAssertNil(repository.connectionId) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "connect completes") repository.connect { receivedError = $0 @@ -131,7 +131,7 @@ final class ConnectionRepository_Tests: XCTestCase { func test_connect_noConnectionId_success() throws { XCTAssertNil(repository.connectionId) - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "connect completes") repository.connect { receivedError = $0 @@ -417,7 +417,7 @@ final class ConnectionRepository_Tests: XCTestCase { let existingConnectionId = "existing-connection-id" repository.completeConnectionIdWaiters(connectionId: existingConnectionId) - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Connection Id Completion") repository.provideConnectionId(timeout: defaultTimeout) { result = $0 @@ -433,7 +433,7 @@ final class ConnectionRepository_Tests: XCTestCase { } func test_connectionId_returnsErrorOnTimeout() { - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Token Completion") repository.provideConnectionId(timeout: 0.01) { result = $0 @@ -459,7 +459,7 @@ final class ConnectionRepository_Tests: XCTestCase { } func test_connectionId_returnsErrorOnMissingValue() { - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Token Completion") repository.provideConnectionId(timeout: defaultTimeout) { result = $0 @@ -475,7 +475,7 @@ final class ConnectionRepository_Tests: XCTestCase { } func test_connectionId_returnsValue_whenCompletingTokenWaiters() { - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Token Completion") repository.provideConnectionId(timeout: defaultTimeout) { result = $0 @@ -522,7 +522,7 @@ final class ConnectionRepository_Tests: XCTestCase { } func test_completeConnectionIdWaiters_valid_connectionId_completesWaiters() { - var result: Result? + nonisolated(unsafe) var result: Result? let expectation = self.expectation(description: "Provide Connection Id Completion") repository.provideConnectionId(timeout: defaultTimeout) { result = $0 diff --git a/Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift b/Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift index ec9f419ad11..50ffbbde7c6 100644 --- a/Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift @@ -47,7 +47,7 @@ final class MessageRepositoryTests: XCTestCase { func test_sendMessage_noChannel() throws { let id = MessageId.unique - let message = try createMessage(id: id, localState: .pendingSend) + nonisolated(unsafe) let message = try createMessage(id: id, localState: .pendingSend) try database.writeSynchronously { _ in message.channel = nil } @@ -63,7 +63,7 @@ final class MessageRepositoryTests: XCTestCase { wait(for: [apiClient.request_expectation], timeout: defaultTimeout) - var currentMessageState: LocalMessageState? + nonisolated(unsafe) var currentMessageState: LocalMessageState? try database.writeSynchronously { session in currentMessageState = session.message(id: id)?.localMessageState } @@ -75,7 +75,7 @@ final class MessageRepositoryTests: XCTestCase { let id = MessageId.unique try createMessage(id: id, localState: .pendingSend) let expectation = self.expectation(description: "Send Message completes") - var result: Result? + nonisolated(unsafe) var result: Result? repository.sendMessage(with: id) { result = $0 expectation.fulfill() @@ -88,7 +88,7 @@ final class MessageRepositoryTests: XCTestCase { wait(for: [expectation], timeout: defaultTimeout) - var currentMessageState: LocalMessageState? + nonisolated(unsafe) var currentMessageState: LocalMessageState? try database.writeSynchronously { session in currentMessageState = session.message(id: id)?.localMessageState } @@ -106,7 +106,7 @@ final class MessageRepositoryTests: XCTestCase { let id = MessageId.unique try createMessage(id: id, localState: .pendingSend) let expectation = self.expectation(description: "Send Message completes") - var result: Result? + nonisolated(unsafe) var result: Result? repository.sendMessage(with: id) { result = $0 expectation.fulfill() @@ -119,7 +119,7 @@ final class MessageRepositoryTests: XCTestCase { wait(for: [expectation], timeout: defaultTimeout) - var currentMessageState: LocalMessageState? + nonisolated(unsafe) var currentMessageState: LocalMessageState? try database.writeSynchronously { session in currentMessageState = session.message(id: id)?.localMessageState } @@ -137,7 +137,7 @@ final class MessageRepositoryTests: XCTestCase { let id = MessageId.unique try createMessage(id: id, localState: .pendingSend) let expectation = self.expectation(description: "Send Message completes") - var result: Result? + nonisolated(unsafe) var result: Result? repository.sendMessage(with: id) { result = $0 expectation.fulfill() @@ -150,7 +150,7 @@ final class MessageRepositoryTests: XCTestCase { wait(for: [expectation], timeout: defaultTimeout) - var currentMessageState: LocalMessageState? + nonisolated(unsafe) var currentMessageState: LocalMessageState? try database.writeSynchronously { session in currentMessageState = session.message(id: id)?.localMessageState } @@ -266,7 +266,7 @@ final class MessageRepositoryTests: XCTestCase { let payload = MessagePayload.dummy(messageId: id, authorUserId: .anonymous, channel: .dummy(cid: cid)) let message = runSaveSuccessfullySentMessageAndWait(payload: payload) let dbMessage = self.message(for: id) - var dbChannel: ChatChannel? + nonisolated(unsafe) var dbChannel: ChatChannel? try database.writeSynchronously { session in dbChannel = try session.channel(cid: self.cid)?.asModel() } @@ -278,7 +278,7 @@ final class MessageRepositoryTests: XCTestCase { private func runSaveSuccessfullySentMessageAndWait(payload: MessagePayload) -> ChatMessage? { let expectation = self.expectation(description: "Save Message completes") - var result: ChatMessage? + nonisolated(unsafe) var result: ChatMessage? repository.saveSuccessfullySentMessage(cid: cid, message: payload) { result = $0.value expectation.fulfill() @@ -320,7 +320,7 @@ final class MessageRepositoryTests: XCTestCase { func test_getMessage_propogatesRequestError() { // Simulate `getMessage(cid:, messageId:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? repository.getMessage(cid: .unique, messageId: .unique, store: true) { completionCalledError = $0.error } @@ -347,7 +347,7 @@ final class MessageRepositoryTests: XCTestCase { database.write_errorResponse = testError // Simulate `getMessage(cid:, messageId:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? repository.getMessage(cid: channelId, messageId: messagePayload.message.id, store: true) { completionCalledError = $0.error } @@ -371,7 +371,7 @@ final class MessageRepositoryTests: XCTestCase { try database.createChannel(cid: cid) // Simulate `getMessage(cid:, messageId:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false repository.getMessage(cid: cid, messageId: messageId, store: true) { _ in completionCalled = true } @@ -401,7 +401,7 @@ final class MessageRepositoryTests: XCTestCase { try database.createChannel(cid: cid) // Simulate `getMessage(cid:, messageId:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false repository.getMessage(cid: cid, messageId: messageId, store: false) { _ in completionCalled = true } @@ -584,7 +584,7 @@ final class MessageRepositoryTests: XCTestCase { private func runSaveSuccessfullyDeletedMessageAndWait(message: MessagePayload) -> Error? { let expectation = self.expectation(description: "Mark Message completes") - var error: Error? + nonisolated(unsafe) var error: Error? repository.saveSuccessfullyDeletedMessage(message: message) { error = $0 expectation.fulfill() @@ -594,7 +594,7 @@ final class MessageRepositoryTests: XCTestCase { } private func message(for id: MessageId) -> ChatMessage? { - var dbMessage: ChatMessage? + nonisolated(unsafe) var dbMessage: ChatMessage? try? database.writeSynchronously { session in dbMessage = try? session.message(id: id)?.asModel() } @@ -640,7 +640,7 @@ final class MessageRepositoryTests: XCTestCase { } waitForExpectations(timeout: defaultTimeout, handler: nil) - var reactionState: LocalReactionState? + nonisolated(unsafe) var reactionState: LocalReactionState? try database.writeSynchronously { session in let reaction = session.reaction(messageId: messageId, userId: userId, type: reactionType) reactionState = reaction?.localState @@ -688,8 +688,8 @@ final class MessageRepositoryTests: XCTestCase { } waitForExpectations(timeout: defaultTimeout, handler: nil) - var reactionState: LocalReactionState? - var reactionScore: Int64? + nonisolated(unsafe) var reactionState: LocalReactionState? + nonisolated(unsafe) var reactionScore: Int64? try database.writeSynchronously { session in let reaction = session.reaction(messageId: messageId, userId: userId, type: reactionType) reactionState = reaction?.localState @@ -712,7 +712,7 @@ extension MessageRepositoryTests { ) throws -> MessageDTO { try database.createCurrentUser() try database.createChannel(cid: cid) - var message: MessageDTO! + nonisolated(unsafe) var message: MessageDTO! try database.writeSynchronously { session in message = try session.createNewMessage( in: self.cid, @@ -734,7 +734,7 @@ extension MessageRepositoryTests { private func runSendMessageAndWait(id: MessageId) -> Result? { let expectation = self.expectation(description: "Send Message completes") - var result: Result? + nonisolated(unsafe) var result: Result? repository.sendMessage(with: id) { result = $0 expectation.fulfill() diff --git a/Tests/StreamChatTests/Repositories/PollsRepository_Tests.swift b/Tests/StreamChatTests/Repositories/PollsRepository_Tests.swift index bbc2b48e343..0aa4dfc87e1 100644 --- a/Tests/StreamChatTests/Repositories/PollsRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/PollsRepository_Tests.swift @@ -347,7 +347,7 @@ final class PollsRepository_Tests: XCTestCase { let pollOptionId = "345" let pollId = "123" let messageId: String = .unique - var voteId: String! + nonisolated(unsafe) var voteId: String! let currentUserId = String.unique let payload = XCTestCase().dummyPollVotePayload(optionId: pollOptionId, pollId: pollId) @@ -389,7 +389,7 @@ final class PollsRepository_Tests: XCTestCase { let pollOptionId = "345" let pollId = "123" let messageId: String = .unique - var voteId: String! + nonisolated(unsafe) var voteId: String! let currentUserId = String.unique let payload = XCTestCase().dummyPollVotePayload(optionId: pollOptionId, pollId: pollId) diff --git a/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift index d7c1f49c729..2c85390a8e5 100644 --- a/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift @@ -502,7 +502,7 @@ extension SyncRepository_Tests { XCTAssertCall("exitRecoveryMode()", on: apiClient) } - private class CancelRecoveryFlowTracker: SyncRepository { + private class CancelRecoveryFlowTracker: SyncRepository, @unchecked Sendable { var cancelRecoveryFlowClosure: () -> Void = {} override func cancelRecoveryFlow() { diff --git a/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift b/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift index 79a9a1af8f3..61f4ef5ae96 100644 --- a/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift @@ -456,7 +456,7 @@ final class ChannelList_Tests: XCTestCase { loadState: Bool = true, filter: Filter? = nil, sort: [Sorting] = [.init(key: .createdAt, isAscending: true)], - dynamicFilter: ((ChatChannel) -> Bool)? = nil + dynamicFilter: (@Sendable(ChatChannel) -> Bool)? = nil ) { channelList = ChannelList( query: ChannelListQuery( diff --git a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift index 61bcc2abeaa..8dc258a565b 100644 --- a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift @@ -1951,7 +1951,7 @@ extension Chat_Tests { static func chatClientEnvironment() -> ChatClient.Environment { var environment = ChatClient.Environment.mock - environment.messageRepositoryBuilder = MessageRepository.init + environment.messageRepositoryBuilder = { MessageRepository(database: $0, apiClient: $1) } return environment } } diff --git a/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift b/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift index 3e360feeaec..9cb2ed8dcf2 100644 --- a/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift @@ -187,7 +187,7 @@ final class ConnectedUser_Tests: XCTestCase { // MARK: - Test Data @MainActor private func setUpConnectedUser(usesMockedUpdaters: Bool, loadState: Bool = true, initialDeviceCount: Int = 0) async throws { - var user: CurrentChatUser! + nonisolated(unsafe) var user: CurrentChatUser! try await env.client.databaseContainer.write { session in user = try session.saveCurrentUser(payload: self.currentUserPayload(deviceCount: initialDeviceCount)).asModel() } diff --git a/Tests/StreamChatTests/Utils/EventBatcher_Tests.swift b/Tests/StreamChatTests/Utils/EventBatcher_Tests.swift index b5e823301ee..d53dc77e7d7 100644 --- a/Tests/StreamChatTests/Utils/EventBatcher_Tests.swift +++ b/Tests/StreamChatTests/Utils/EventBatcher_Tests.swift @@ -7,12 +7,12 @@ import XCTest final class Batch_Tests: XCTestCase { - var time: VirtualTime { VirtualTimeTimer.time } + var time: VirtualTime { VirtualTimeTimer.time.value! } override func setUp() { super.setUp() - VirtualTimeTimer.time = .init() + VirtualTimeTimer.time.value = .init() } override func tearDown() { diff --git a/Tests/StreamChatTests/WebSocketClient/BackgroundTaskScheduler_Tests.swift b/Tests/StreamChatTests/WebSocketClient/BackgroundTaskScheduler_Tests.swift index daecce7cb6b..8272d180c8c 100644 --- a/Tests/StreamChatTests/WebSocketClient/BackgroundTaskScheduler_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/BackgroundTaskScheduler_Tests.swift @@ -77,7 +77,7 @@ final class IOSBackgroundTaskScheduler_Tests: XCTestCase { // MARK: - Mocks - class IOSBackgroundTaskSchedulerMock: IOSBackgroundTaskScheduler { + class IOSBackgroundTaskSchedulerMock: IOSBackgroundTaskScheduler, @unchecked Sendable { let endTaskClosure: () -> Void init(endTaskClosure: @escaping () -> Void) { diff --git a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift index 27b6c09cbb4..7c6623fc070 100644 --- a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift @@ -723,7 +723,7 @@ final class ChannelReadUpdaterMiddleware_Tests: XCTestCase { ) let oldMessageNewEvent = try NotificationMessageNewEventDTO(from: eldEventPayload) - var handledEvent: Event? + nonisolated(unsafe) var handledEvent: Event? try database.writeSynchronously { session in // Let the middleware handle the event // Middleware should mutate the loadedChannel's read diff --git a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/EventDTOConverterMiddleware_Tests.swift b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/EventDTOConverterMiddleware_Tests.swift index 5045ec8fe5b..e87cdd0a446 100644 --- a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/EventDTOConverterMiddleware_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/EventDTOConverterMiddleware_Tests.swift @@ -24,11 +24,11 @@ final class EventDTOConverterMiddleware_Tests: XCTestCase { } func test_handle_whenEventDTOComes_toDomainResultIsReturned() throws { - class EventDTOMock: EventDTO { + final class EventDTOMock: EventDTO { let payload = EventPayload(eventType: .channelDeleted) - var toDomainEvent_session: DatabaseSession? - var toDomainEvent_returnValue: Event? + nonisolated(unsafe) var toDomainEvent_session: DatabaseSession? + nonisolated(unsafe) var toDomainEvent_returnValue: Event? func toDomainEvent(session: DatabaseSession) -> Event? { toDomainEvent_session = session diff --git a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/MemberEventMiddleware_Tests.swift b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/MemberEventMiddleware_Tests.swift index 523c28a1394..86f03c95359 100644 --- a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/MemberEventMiddleware_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/MemberEventMiddleware_Tests.swift @@ -85,7 +85,7 @@ final class MemberEventMiddleware_Tests: XCTestCase { let channelListObserver = TestChannelListObserver(database: database) // Simulate `MemberAddedEvent` event. - var forwardedEvent: Event? + nonisolated(unsafe) var forwardedEvent: Event? try database.writeSynchronously { session in forwardedEvent = self.middleware.handle(event: event, session: session) } @@ -416,7 +416,7 @@ final class MemberEventMiddleware_Tests: XCTestCase { let event = try MemberUpdatedEventDTO(from: eventPayload) // Simulate `MemberUpdatedEvent` event. - var forwardedEvent: Event? + nonisolated(unsafe) var forwardedEvent: Event? try database.writeSynchronously { session in forwardedEvent = self.middleware.handle(event: event, session: session) } @@ -472,7 +472,7 @@ final class MemberEventMiddleware_Tests: XCTestCase { let channelListObserver = TestChannelListObserver(database: database) // Simulate `NotificationAddedToChannelEvent` event. - var forwardedEvent: Event? + nonisolated(unsafe) var forwardedEvent: Event? try database.writeSynchronously { session in forwardedEvent = self.middleware.handle(event: event, session: session) } @@ -616,7 +616,7 @@ final class MemberEventMiddleware_Tests: XCTestCase { let channelListObserver = TestChannelListObserver(database: database) // Simulate `NotificationAddedToChannelEvent` event. - var forwardedEvent: Event? + nonisolated(unsafe) var forwardedEvent: Event? try database.writeSynchronously { session in forwardedEvent = self.middleware.handle(event: event, session: session) } @@ -712,7 +712,7 @@ final class MemberEventMiddleware_Tests: XCTestCase { let channelListObserver = TestChannelListObserver(database: database) // Simulate `NotificationAddedToChannelEvent` event. - var forwardedEvent: Event? + nonisolated(unsafe) var forwardedEvent: Event? try database.writeSynchronously { session in forwardedEvent = self.middleware.handle(event: event, session: session) } @@ -765,7 +765,7 @@ final class MemberEventMiddleware_Tests: XCTestCase { let channelListObserver = TestChannelListObserver(database: database) // Simulate `NotificationAddedToChannelEvent` event. - var forwardedEvent: Event? + nonisolated(unsafe) var forwardedEvent: Event? try database.writeSynchronously { session in forwardedEvent = self.middleware.handle(event: event, session: session) } diff --git a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/TypingStartCleanupMiddleware_Tests.swift b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/TypingStartCleanupMiddleware_Tests.swift index 654d78ca588..a8ddaff4a01 100644 --- a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/TypingStartCleanupMiddleware_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/TypingStartCleanupMiddleware_Tests.swift @@ -19,7 +19,7 @@ final class TypingStartCleanupMiddleware_Tests: XCTestCase { currentUser = .mock(id: "Luke") time = VirtualTime() - VirtualTimeTimer.time = time + VirtualTimeTimer.time.value = time database = DatabaseContainer_Spy() try database.writeSynchronously { session in diff --git a/Tests/StreamChatTests/WebSocketClient/RetryStrategy_Tests.swift b/Tests/StreamChatTests/WebSocketClient/RetryStrategy_Tests.swift index b106c49f755..b657ab454de 100644 --- a/Tests/StreamChatTests/WebSocketClient/RetryStrategy_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/RetryStrategy_Tests.swift @@ -87,8 +87,8 @@ final class RetryStrategy_Tests: XCTestCase { // Create mock strategy struct MockStrategy: RetryStrategy { let consecutiveFailuresCount: Int = 0 - let incrementConsecutiveFailuresClosure: () -> Void - let nextRetryDelayClosure: () -> Void + let incrementConsecutiveFailuresClosure: @Sendable() -> Void + let nextRetryDelayClosure: @Sendable() -> Void func resetConsecutiveFailures() {} @@ -103,8 +103,8 @@ final class RetryStrategy_Tests: XCTestCase { } // Create mock strategy instance and catch `incrementConsecutiveFailures/nextRetryDelay` calls - var incrementConsecutiveFailuresCalled = false - var nextRetryDelayClosure = false + nonisolated(unsafe) var incrementConsecutiveFailuresCalled = false + nonisolated(unsafe) var nextRetryDelayClosure = false var strategy = MockStrategy( incrementConsecutiveFailuresClosure: { diff --git a/Tests/StreamChatTests/WebSocketClient/WebSocketClient_Tests.swift b/Tests/StreamChatTests/WebSocketClient/WebSocketClient_Tests.swift index 2443a29cf79..baaa0ebcd44 100644 --- a/Tests/StreamChatTests/WebSocketClient/WebSocketClient_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/WebSocketClient_Tests.swift @@ -32,7 +32,7 @@ final class WebSocketClient_Tests: XCTestCase { super.setUp() time = VirtualTime() - VirtualTimeTimer.time = time + VirtualTimeTimer.time.value = time endpoint = .webSocketConnect( userInfo: UserInfo(id: .unique) diff --git a/Tests/StreamChatTests/WebSocketClient/WebSocketPingController_Tests.swift b/Tests/StreamChatTests/WebSocketClient/WebSocketPingController_Tests.swift index b3df799f438..edda3e1a9b6 100644 --- a/Tests/StreamChatTests/WebSocketClient/WebSocketPingController_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/WebSocketPingController_Tests.swift @@ -14,7 +14,7 @@ final class WebSocketPingController_Tests: XCTestCase { override func setUp() { super.setUp() time = VirtualTime() - VirtualTimeTimer.time = time + VirtualTimeTimer.time.value = time pingController = .init(timerType: VirtualTimeTimer.self, timerQueue: .main) delegate = WebSocketPingController_Delegate() diff --git a/Tests/StreamChatTests/Workers/Background/ConnectionRecoveryHandler_Tests.swift b/Tests/StreamChatTests/Workers/Background/ConnectionRecoveryHandler_Tests.swift index 9f3bd5c5883..4beeee508ed 100644 --- a/Tests/StreamChatTests/Workers/Background/ConnectionRecoveryHandler_Tests.swift +++ b/Tests/StreamChatTests/Workers/Background/ConnectionRecoveryHandler_Tests.swift @@ -13,12 +13,12 @@ final class ConnectionRecoveryHandler_Tests: XCTestCase { var mockInternetConnection: InternetConnection_Mock! var mockBackgroundTaskScheduler: BackgroundTaskScheduler_Mock! var mockRetryStrategy: RetryStrategy_Spy! - var mockTime: VirtualTime { VirtualTimeTimer.time } + var mockTime: VirtualTime { VirtualTimeTimer.time.value! } override func setUp() { super.setUp() - VirtualTimeTimer.time = .init() + VirtualTimeTimer.time.value = .init() mockChatClient = ChatClient_Mock(config: .init(apiKeyString: .unique)) mockBackgroundTaskScheduler = BackgroundTaskScheduler_Mock() @@ -240,7 +240,7 @@ final class ConnectionRecoveryHandler_Tests: XCTestCase { /// 2. app -> background (no disconnect, background task is started, no timer) /// 3. bg task -> killed (disconnect) /// 3. app -> foregorund (reconnect) - func test_socketIsConnected_appBackgroundTaskKilledAppForeground() { + func test_socketIsConnected_appBackgroundTaskKilledAppForeground() async throws { // Create handler active in background handler = makeConnectionRecoveryHandler(keepConnectionAliveInBackground: true) @@ -258,7 +258,9 @@ final class ConnectionRecoveryHandler_Tests: XCTestCase { XCTAssertTrue(mockTime.scheduledTimers.isEmpty) // Backgroud task killed - mockBackgroundTaskScheduler.beginBackgroundTask_expirationHandler?() + try await Task.mainActor { + self.mockBackgroundTaskScheduler.beginBackgroundTask_expirationHandler?() + }.value // Assert disconnection is initiated by the system XCTAssertEqual(mockChatClient.mockWebSocketClient.disconnect_source, .systemInitiated) diff --git a/Tests/StreamChatTests/Workers/Background/MessageSender_Tests.swift b/Tests/StreamChatTests/Workers/Background/MessageSender_Tests.swift index 9a14b646dcd..8ee28dc11bd 100644 --- a/Tests/StreamChatTests/Workers/Background/MessageSender_Tests.swift +++ b/Tests/StreamChatTests/Workers/Background/MessageSender_Tests.swift @@ -63,7 +63,7 @@ final class MessageSender_Tests: XCTestCase { func test_senderSendsMessage_withPendingSendLocalState_and_uploadedOrEmptyAttachments() throws { let message1Id: MessageId = .unique - var message2Id: MessageId! + nonisolated(unsafe) var message2Id: MessageId! let message = ChatMessage.mock(id: message1Id, cid: cid, text: "Message sent", author: .unique) messageRepository.sendMessageResult = .success(message) @@ -150,7 +150,7 @@ final class MessageSender_Tests: XCTestCase { } func test_sender_sendsMessage_withUploadedAttachments() throws { - var messageId: MessageId! + nonisolated(unsafe) var messageId: MessageId! try database.writeSynchronously { session in let message = try session.createNewMessage( @@ -178,7 +178,7 @@ final class MessageSender_Tests: XCTestCase { } func test_sender_sendsMessage_withBothNotUploadableAttachmentAndUploadedAttachments() throws { - var messageId: MessageId! + nonisolated(unsafe) var messageId: MessageId! try database.writeSynchronously { session in let message = try session.createNewMessage( @@ -216,9 +216,9 @@ final class MessageSender_Tests: XCTestCase { } func test_senderSendsMessage_inTheOrderTheyWereCreatedLocally() throws { - var message1Id: MessageId! - var message2Id: MessageId! - var message3Id: MessageId! + nonisolated(unsafe) var message1Id: MessageId! + nonisolated(unsafe) var message2Id: MessageId! + nonisolated(unsafe) var message3Id: MessageId! // Create 3 messages in the DB, all with `.pendingSend` local state try database.writeSynchronously { session in @@ -294,11 +294,11 @@ final class MessageSender_Tests: XCTestCase { let cidB = ChannelId.unique try database.createChannel(cid: cidB) - var channelA_message1: MessageId! - var channelA_message2: MessageId! + nonisolated(unsafe) var channelA_message1: MessageId! + nonisolated(unsafe) var channelA_message2: MessageId! - var channelB_message1: MessageId! - var channelB_message2: MessageId! + nonisolated(unsafe) var channelB_message1: MessageId! + nonisolated(unsafe) var channelB_message2: MessageId! // Create 2 new messages in two channel the DB try database.writeSynchronously { session in @@ -387,7 +387,7 @@ final class MessageSender_Tests: XCTestCase { } func test_sender_sendsMessage_whenError_sendsEvent() throws { - var messageId: MessageId! + nonisolated(unsafe) var messageId: MessageId! struct MockError: Error {} messageRepository.sendMessageResult = .failure(.failedToSendMessage(MockError())) diff --git a/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift b/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift index aa8d19f08ff..82ec9477794 100644 --- a/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift @@ -51,7 +51,7 @@ final class ChannelListUpdater_Tests: XCTestCase { func test_update_successfulResponseData_areSavedToDB() { // Simulate `update` call let query = ChannelListQuery(filter: .in(.members, values: [.unique])) - var completionCalled = false + nonisolated(unsafe) var completionCalled = false listUpdater.update(channelListQuery: query, completion: { result in XCTAssertNil(result.error) completionCalled = true @@ -76,7 +76,7 @@ final class ChannelListUpdater_Tests: XCTestCase { func test_update_errorResponse_isPropagatedToCompletion() { // Simulate `update` call let query = ChannelListQuery(filter: .in(.members, values: [.unique])) - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? listUpdater.update(channelListQuery: query, completion: { completionCalledError = $0.error }) // Simulate API response with failure @@ -98,7 +98,7 @@ final class ChannelListUpdater_Tests: XCTestCase { func test_update_savesQuery_onEmptyResponse() { // Simulate `update` call let query = ChannelListQuery(filter: .in(.members, values: [.unique])) - var completionCalled = false + nonisolated(unsafe) var completionCalled = false listUpdater.update(channelListQuery: query, completion: { result in XCTAssertNil(result.error) completionCalled = true @@ -120,7 +120,7 @@ final class ChannelListUpdater_Tests: XCTestCase { } func test_update_whenSuccess_whenFirstFetch_shouldRemoveAllPreviousChannelsFromQuery() throws { - var query = ChannelListQuery( + nonisolated(unsafe) var query = ChannelListQuery( filter: .in(.members, values: [.unique]) ) query.pagination = .init(pageSize: 25, offset: 0) @@ -156,7 +156,7 @@ final class ChannelListUpdater_Tests: XCTestCase { } func test_update_whenSuccess_whenNotFirstFetch_shouldContinueChannelsFromQuery() throws { - var query = ChannelListQuery( + nonisolated(unsafe) var query = ChannelListQuery( filter: .in(.members, values: [.unique]) ) query.pagination = .init(pageSize: 25, offset: 25) @@ -192,7 +192,7 @@ final class ChannelListUpdater_Tests: XCTestCase { } func test_update_whenError_shouldContinueChannelsFromQuery() throws { - var query = ChannelListQuery( + nonisolated(unsafe) var query = ChannelListQuery( filter: .in(.members, values: [.unique]) ) query.pagination = .init(pageSize: 25, offset: 25) @@ -240,7 +240,7 @@ final class ChannelListUpdater_Tests: XCTestCase { func test_fetch_successfulResponse_isPropagatedToCompletion() { // Simulate `fetch` call let query = ChannelListQuery(filter: .in(.members, values: [.unique])) - var channelListPayload: ChannelListPayload? + nonisolated(unsafe) var channelListPayload: ChannelListPayload? listUpdater.fetch(channelListQuery: query, completion: { result in channelListPayload = try? result.get() }) @@ -259,7 +259,7 @@ final class ChannelListUpdater_Tests: XCTestCase { func test_fetch_errorResponse_isPropagatedToCompletion() { // Simulate `fetch` call let query = ChannelListQuery(filter: .in(.members, values: [.unique])) - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? listUpdater.update(channelListQuery: query, completion: { completionCalledError = $0.error }) // Simulate API response with failure @@ -291,7 +291,7 @@ final class ChannelListUpdater_Tests: XCTestCase { func test_refreshLoadedChannels_whenMultiplePagesAreLoaded_thenAllPagesAreReloaded() async throws { let pageSize = Int.channelsPageSize - var query = ChannelListQuery(filter: .in(.members, values: [.unique])) + nonisolated(unsafe) var query = ChannelListQuery(filter: .in(.members, values: [.unique])) query.pagination = Pagination(pageSize: pageSize) let initialChannels = (0..? + nonisolated(unsafe) var completionResult: Result? updater.partialUpdate( userId: .unique, in: cid, @@ -194,7 +194,7 @@ final class ChannelMemberUpdater_Tests: XCTestCase { func test_partialUpdate_propagatesError() { // Simulate `partialUpdate` call - var completionResult: Result? + nonisolated(unsafe) var completionResult: Result? updater.partialUpdate( userId: .unique, in: .unique, diff --git a/Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift b/Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift index 21b1f5bbaf3..05842db0437 100644 --- a/Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift @@ -69,7 +69,7 @@ final class ChannelUpdater_Tests: XCTestCase { let expectedPaginationParameter = PaginationParameter.lessThan(.unique) let query = ChannelQuery(cid: .unique, paginationParameter: expectedPaginationParameter) let expectation = self.expectation(description: "Update completes") - var updateResult: Result! + nonisolated(unsafe) var updateResult: Result! channelUpdater.update(channelQuery: query, isInRecoveryMode: false, completion: { result in updateResult = result expectation.fulfill() @@ -100,7 +100,7 @@ final class ChannelUpdater_Tests: XCTestCase { // Simulate `update(channelQuery:)` call let query = ChannelQuery(cid: .unique) let expectation = self.expectation(description: "Update completes") - var updateResult: Result! + nonisolated(unsafe) var updateResult: Result! channelUpdater.update(channelQuery: query, isInRecoveryMode: false, completion: { result in updateResult = result expectation.fulfill() @@ -222,7 +222,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_updateChannelQuery_errorResponse_isPropagatedToCompletion() { // Simulate `update(channelQuery:)` call let query = ChannelQuery(cid: .unique) - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.update(channelQuery: query, isInRecoveryMode: false, completion: { completionCalledError = $0.error }) // Simulate API response with failure @@ -236,7 +236,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_updateChannelQueryRecovery_errorResponse_isPropagatedToCompletion() { // Simulate `update(channelQuery:)` call let query = ChannelQuery(cid: .unique) - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.update(channelQuery: query, isInRecoveryMode: true, completion: { completionCalledError = $0.error }) // Simulate API response with failure @@ -250,13 +250,13 @@ final class ChannelUpdater_Tests: XCTestCase { func test_updateChannelQuery_completionForCreatedChannelCalled() { // Simulate `update(channelQuery:)` call let query = ChannelQuery(channelPayload: .unique) - var cid: ChannelId = .unique + nonisolated(unsafe) var cid: ChannelId = .unique var channel: ChatChannel? { try? database.viewContext.channel(cid: cid)?.asModel() } - let callback: (ChannelId) -> Void = { + let callback: @Sendable(ChannelId) -> Void = { cid = $0 // Assert channel is not saved to DB before callback returns AssertAsync.staysTrue(channel == nil) @@ -284,13 +284,13 @@ final class ChannelUpdater_Tests: XCTestCase { func test_updateChannelQueryRecovery_completionForCreatedChannelCalled() { // Simulate `update(channelQuery:)` call let query = ChannelQuery(channelPayload: .unique) - var cid: ChannelId = .unique + nonisolated(unsafe) var cid: ChannelId = .unique var channel: ChatChannel? { try? database.viewContext.channel(cid: cid)?.asModel() } - let callback: (ChannelId) -> Void = { + let callback: @Sendable(ChannelId) -> Void = { cid = $0 // Assert channel is not saved to DB before callback returns AssertAsync.staysTrue(channel == nil) @@ -688,7 +688,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_updateChannel_successfulResponse_isPropagatedToCompletion() { // Simulate `updateChannel(channelPayload:, completion:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.updateChannel(channelPayload: .unique) { error in XCTAssertNil(error) completionCalled = true @@ -706,7 +706,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_updateChannel_errorResponse_isPropagatedToCompletion() { // Simulate `updateChannel(channelPayload:, completion:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.updateChannel(channelPayload: .unique) { completionCalledError = $0 } // Simulate API response with failure @@ -771,7 +771,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_partialChannelUpdate_successfulResponse_isPropagatedToCompletion() { // Simulate `partialChannelUpdate(updates:unsetProperties:completion:)` call - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "partialChannelUpdate completion") channelUpdater.partialChannelUpdate(updates: .unique, unsetProperties: []) { error in receivedError = error @@ -787,7 +787,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_partialChannelUpdate_errorResponse_isPropagatedToCompletion() { // Simulate `partialChannelUpdate(updates:unsetProperties:completion:)` call - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? let expectation = self.expectation(description: "partialChannelUpdate completion") channelUpdater.partialChannelUpdate(updates: .unique, unsetProperties: []) { error in receivedError = error @@ -831,7 +831,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_muteChannel_successfulResponse_isPropagatedToCompletion() { // Simulate `muteChannel(cid:, mute:, completion:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.muteChannel(cid: .unique, mute: true) { error in XCTAssertNil(error) completionCalled = true @@ -851,7 +851,7 @@ final class ChannelUpdater_Tests: XCTestCase { let expiration = 1_000_000 // Simulate `muteChannel(cid:, mute:, completion:, expiration:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.muteChannel(cid: .unique, mute: true, expiration: expiration) { error in XCTAssertNil(error) completionCalled = true @@ -869,7 +869,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_muteChannel_errorResponse_isPropagatedToCompletion() { // Simulate `muteChannel(cid:, mute:, completion:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.muteChannel(cid: .unique, mute: true) { completionCalledError = $0 } // Simulate API response with failure @@ -884,7 +884,7 @@ final class ChannelUpdater_Tests: XCTestCase { let expiration = 1_000_000 // Simulate `muteChannel(cid:, mute:, completion:, expiration:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.muteChannel(cid: .unique, mute: true, expiration: expiration) { completionCalledError = $0 } // Simulate API response with failure @@ -910,7 +910,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_deleteChannel_successfulResponse_isPropagatedToCompletion() { // Simulate `deleteChannel(cid:, completion:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.deleteChannel(cid: .unique) { error in XCTAssertNil(error) completionCalled = true @@ -928,7 +928,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_deleteChannel_errorResponse_isPropagatedToCompletion() { // Simulate `deleteChannel(cid:, completion:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.deleteChannel(cid: .unique) { completionCalledError = $0 } // Simulate API response with failure @@ -1041,7 +1041,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_truncateChannel_successfulResponse_isPropagatedToCompletion() { // Simulate `truncateChannel(cid:, completion:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.truncateChannel(cid: .unique) { error in XCTAssertNil(error) completionCalled = true @@ -1059,7 +1059,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_truncateChannel_errorResponse_isPropagatedToCompletion() { // Simulate `truncateChannel(cid:, completion:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.truncateChannel(cid: .unique) { completionCalledError = $0 } // Simulate API response with failure @@ -1136,7 +1136,7 @@ final class ChannelUpdater_Tests: XCTestCase { XCTAssertEqual(channel?.isHidden, false) // Simulate `hideChannel(cid:, clearHistory:, completion:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.hideChannel(cid: .unique, clearHistory: true) { completionCalledError = $0 } // Simulate API response with failure @@ -1165,7 +1165,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_showChannel_successfulResponse_isPropagatedToCompletion() { // Simulate `showChannel(cid:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.showChannel(cid: .unique) { error in XCTAssertNil(error) completionCalled = true @@ -1183,7 +1183,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_showChannel_errorResponse_isPropagatedToCompletion() { // Simulate `showChannel(cid:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.showChannel(cid: .unique) { completionCalledError = $0 } // Simulate API response with failure @@ -1288,7 +1288,7 @@ final class ChannelUpdater_Tests: XCTestCase { let userIds: Set = Set([UserId.unique]) // Simulate `addMembers(cid:, mute:, userIds:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.addMembers( cid: channelID, members: userIds.map { MemberInfo(userId: $0, extraData: nil) }, @@ -1312,7 +1312,7 @@ final class ChannelUpdater_Tests: XCTestCase { let channelID = ChannelId.unique let userIds: Set = Set([UserId.unique]) - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.addMembers( cid: channelID, members: userIds.map { MemberInfo(userId: $0, extraData: nil) }, @@ -1348,7 +1348,7 @@ final class ChannelUpdater_Tests: XCTestCase { let userIds: Set = Set([UserId.unique]) // Simulate `inviteMembers(cid:, mute:, userIds:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.inviteMembers(cid: channelID, userIds: userIds) { error in XCTAssertNil(error) completionCalled = true @@ -1369,7 +1369,7 @@ final class ChannelUpdater_Tests: XCTestCase { let userIds: Set = Set([UserId.unique]) // Simulate `inviteMembers(cid:, channelID:, userIds:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.inviteMembers(cid: channelID, userIds: userIds) { completionCalledError = $0 } // Simulate API response with failure @@ -1398,7 +1398,7 @@ final class ChannelUpdater_Tests: XCTestCase { let message = "Hooray" // Simulate `acceptInvite(cid:, mute:, userIds:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.acceptInvite(cid: channelID, message: message) { error in XCTAssertNil(error) completionCalled = true @@ -1417,7 +1417,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_acceptInvite_errorResponse_isPropagatedToCompletion() { let channelID = ChannelId.unique - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.acceptInvite(cid: channelID, message: "Hooray") { completionCalledError = $0 } // Simulate API response with failure @@ -1444,7 +1444,7 @@ final class ChannelUpdater_Tests: XCTestCase { let channelID = ChannelId.unique // Simulate `rejectInvite(cid:, mute:, userIds:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.rejectInvite(cid: channelID) { error in XCTAssertNil(error) completionCalled = true @@ -1463,7 +1463,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_rejectInvite_errorResponse_isPropagatedToCompletion() { let channelID = ChannelId.unique - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.rejectInvite(cid: channelID) { completionCalledError = $0 } // Simulate API response with failure @@ -1526,7 +1526,7 @@ final class ChannelUpdater_Tests: XCTestCase { let userIds: Set = Set([UserId.unique]) // Simulate `removeMembers(cid:, mute:, userIds:)` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.removeMembers(cid: channelID, userIds: userIds) { error in XCTAssertNil(error) completionCalled = true @@ -1547,7 +1547,7 @@ final class ChannelUpdater_Tests: XCTestCase { let userIds: Set = Set([UserId.unique]) // Simulate `removeMembers(cid:, mute:, completion:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.removeMembers(cid: channelID, userIds: userIds) { completionCalledError = $0 } // Simulate API response with failure @@ -1572,7 +1572,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_markRead_successfulResponse_isPropagatedToCompletion() { let expectation = self.expectation(description: "markRead completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? channelRepository.markReadResult = .success(()) channelUpdater.markRead(cid: .unique, userId: .unique) { error in @@ -1587,7 +1587,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_markRead_errorResponse_isPropagatedToCompletion() { let expectation = self.expectation(description: "markRead completes") let mockedError = TestError() - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? channelRepository.markReadResult = .failure(mockedError) channelUpdater.markRead(cid: .unique, userId: .unique) { error in @@ -1617,7 +1617,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_markUnread_successfulResponse_isPropagatedToCompletion() { let expectation = self.expectation(description: "markUnread completes") - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? channelRepository.markUnreadResult = .success(.mock(cid: .unique)) channelUpdater.markUnread(cid: .unique, userId: .unique, from: .unique, lastReadMessageId: .unique) { result in @@ -1632,7 +1632,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_markUnread_errorResponse_isPropagatedToCompletion() { let expectation = self.expectation(description: "markUnread completes") let mockedError = TestError() - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? channelRepository.markUnreadResult = .failure(mockedError) channelUpdater.markUnread(cid: .unique, userId: .unique, from: .unique, lastReadMessageId: .unique) { result in @@ -1657,7 +1657,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_enableSlowMode_successfulResponse_isPropagatedToCompletion() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.enableSlowMode(cid: .unique, cooldownDuration: .random(in: 0...120)) { error in XCTAssertNil(error) completionCalled = true @@ -1671,7 +1671,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_enableSlowMode_errorResponse_isPropagatedToCompletion() { - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.enableSlowMode(cid: .unique, cooldownDuration: .random(in: 0...120)) { completionCalledError = $0 } let error = TestError() @@ -1705,7 +1705,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_startWatching_successfulResponse_isPropagatedToCompletion() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let cid = ChannelId.unique channelUpdater.startWatching(cid: cid, isInRecoveryMode: false) { error in XCTAssertNil(error) @@ -1722,7 +1722,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_startWatchingRecovery_successfulResponse_isPropagatedToCompletion() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let cid = ChannelId.unique channelUpdater.startWatching(cid: cid, isInRecoveryMode: true) { error in XCTAssertNil(error) @@ -1739,7 +1739,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_startWatching_errorResponse_isPropagatedToCompletion() { - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.startWatching(cid: .unique, isInRecoveryMode: false) { completionCalledError = $0 } let error = TestError() @@ -1749,7 +1749,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_startWatchingRecovery_errorResponse_isPropagatedToCompletion() { - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.startWatching(cid: .unique, isInRecoveryMode: true) { completionCalledError = $0 } let error = TestError() @@ -1771,7 +1771,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_stopWatching_successfulResponse_isPropagatedToCompletion() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let cid = ChannelId.unique channelUpdater.stopWatching(cid: cid) { error in XCTAssertNil(error) @@ -1786,7 +1786,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_stopWatching_errorResponse_isPropagatedToCompletion() { - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.stopWatching(cid: .unique) { completionCalledError = $0 } let error = TestError() @@ -1809,7 +1809,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_channelWatchers_successfulResponse_isPropagatedToCompletion() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let cid = ChannelId.unique let query = ChannelWatcherListQuery(cid: cid) channelUpdater.channelWatchers(query: query) { result in @@ -1827,7 +1827,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_channelWatchers_errorResponse_isPropagatedToCompletion() { - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? let query = ChannelWatcherListQuery(cid: .unique) channelUpdater.channelWatchers(query: query) { completionCalledError = $0.error } @@ -1885,7 +1885,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_freezeChannel_successfulResponse_isPropagatedToCompletion() { - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let cid = ChannelId.unique let freeze = Bool.random() channelUpdater.freezeChannel(freeze, cid: cid) { error in @@ -1901,7 +1901,7 @@ final class ChannelUpdater_Tests: XCTestCase { } func test_freezeChannel_errorResponse_isPropagatedToCompletion() { - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.freezeChannel(.random(), cid: .unique) { completionCalledError = $0 } let error = TestError() @@ -1927,7 +1927,7 @@ final class ChannelUpdater_Tests: XCTestCase { let cid = ChannelId.unique let type = AttachmentType.image - var completionCalled = false + nonisolated(unsafe) var completionCalled = false channelUpdater.uploadFile(type: type, localFileURL: .localYodaImage, cid: cid) { result in do { let uploadedAttachment = try result.get() @@ -1952,7 +1952,7 @@ final class ChannelUpdater_Tests: XCTestCase { let cid = ChannelId.unique let type = AttachmentType.image - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? channelUpdater.uploadFile(type: type, localFileURL: .localYodaImage, cid: cid) { result in do { _ = try result.get() @@ -1999,7 +1999,7 @@ final class ChannelUpdater_Tests: XCTestCase { let query = PinnedMessagesQuery(pageSize: 10, pagination: .aroundMessage(.unique)) // Simulate `loadPinnedMessages` call - var completionPayload: [ChatMessage]? + nonisolated(unsafe) var completionPayload: [ChatMessage]? channelUpdater.loadPinnedMessages(in: cid, query: query) { completionPayload = try? $0.get() } @@ -2022,7 +2022,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_loadPinnedMessages_propagatesErrorToCompletion() { // Simulate `loadPinnedMessages` call - var completionError: Error? + nonisolated(unsafe) var completionError: Error? channelUpdater.loadPinnedMessages(in: .unique, query: .init(pageSize: 10, pagination: nil)) { completionError = $0.error } @@ -2048,7 +2048,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_enrichUrl_whenSuccess() { let exp = expectation(description: "enrichUrl completes") let url = URL(string: "www.google.com")! - var linkPayload: LinkAttachmentPayload? + nonisolated(unsafe) var linkPayload: LinkAttachmentPayload? channelUpdater.enrichUrl(url) { result in XCTAssertNil(result.error) linkPayload = result.value @@ -2069,7 +2069,7 @@ final class ChannelUpdater_Tests: XCTestCase { func test_enrichUrl_whenFailure() { let exp = expectation(description: "enrichUrl completes") let url = URL(string: "www.google.com")! - var linkPayload: LinkAttachmentPayload? + nonisolated(unsafe) var linkPayload: LinkAttachmentPayload? channelUpdater.enrichUrl(url) { result in XCTAssertNotNil(result.error) linkPayload = result.value diff --git a/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift b/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift index 7771b46b5d2..7dd6919fe7f 100644 --- a/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift @@ -159,7 +159,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { let expectedRole = UserRole.anonymous // Call update user - var completionCalled = false + nonisolated(unsafe) var completionCalled = false currentUserUpdater.updateUserData( currentUserId: expectedId, name: expectedName, @@ -215,7 +215,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { } // Call update user - var completionError: Error? + nonisolated(unsafe) var completionError: Error? currentUserUpdater.updateUserData( currentUserId: userPayload.id, name: .unique, @@ -280,7 +280,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { database.write_errorResponse = testError // Call update user - var completionError: Error? + nonisolated(unsafe) var completionError: Error? currentUserUpdater.updateUserData( currentUserId: .unique, name: .unique, @@ -356,7 +356,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { apiClient.test_mockResponseResult(Result.failure(error)) // Call addDevice - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? currentUserUpdater.addDevice( deviceId: "test", pushProvider: .apn, @@ -476,7 +476,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { let expectation = XCTestExpectation() // Call removeDevice - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? currentUserUpdater.removeDevice(id: "", currentUserId: .unique) { completionCalledError = $0 expectation.fulfill() @@ -556,7 +556,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { } // Call updateDevices - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? currentUserUpdater.fetchDevices(currentUserId: .unique) { completionCalledError = $0.error } @@ -592,7 +592,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { database.write_errorResponse = testError // Call updateDevices - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? currentUserUpdater.fetchDevices(currentUserId: .unique) { completionCalledError = $0.error } @@ -630,7 +630,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { let apiDevices = dummyDevices.devices.map { Device(id: $0.id, createdAt: $0.createdAt) } // Call updateDevices - var callbackCalled = false + nonisolated(unsafe) var callbackCalled = false currentUserUpdater.fetchDevices(currentUserId: .unique) { result in XCTAssertEqual(result, success: apiDevices) callbackCalled = true @@ -665,7 +665,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { func test_markAllRead_successfulResponse_isPropagatedToCompletion() { // GIVEN - var completionCalled = false + nonisolated(unsafe) var completionCalled = false // WHEN currentUserUpdater.markAllRead { error in @@ -681,7 +681,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { func test_markAllRead_errorResponse_isPropagatedToCompletion() { // GIVEN - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? let error = TestError() // WHEN @@ -726,7 +726,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { func test_loadAllUnreads_makesCorrectAPICall() { // Call loadAllUnreads - var receivedUnreads: CurrentUserUnreads? + nonisolated(unsafe) var receivedUnreads: CurrentUserUnreads? currentUserUpdater.loadAllUnreads { result in receivedUnreads = try? result.get() } @@ -784,7 +784,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { func test_loadAllUnreads_propagatesNetworkError() { // Call loadAllUnreads - var receivedError: Error? + nonisolated(unsafe) var receivedError: Error? currentUserUpdater.loadAllUnreads { result in if case let .failure(error) = result { receivedError = error diff --git a/Tests/StreamChatTests/Workers/EventNotificationCenter_Tests.swift b/Tests/StreamChatTests/Workers/EventNotificationCenter_Tests.swift index c41467811d9..a3c82d8eb62 100644 --- a/Tests/StreamChatTests/Workers/EventNotificationCenter_Tests.swift +++ b/Tests/StreamChatTests/Workers/EventNotificationCenter_Tests.swift @@ -135,7 +135,7 @@ final class EventNotificationCenter_Tests: XCTestCase { let events = [TestEvent(), TestEvent(), TestEvent(), TestEvent()] // Feed events that should be posted and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false center.process(events, postNotifications: true) { completionCalled = true } @@ -158,7 +158,7 @@ final class EventNotificationCenter_Tests: XCTestCase { let events = [TestEvent(), TestEvent(), TestEvent(), TestEvent()] // Feed events that should not be posted and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false center.process(events, postNotifications: false) { completionCalled = true } diff --git a/Tests/StreamChatTests/Workers/EventObservers/EventObserver_Tests.swift b/Tests/StreamChatTests/Workers/EventObservers/EventObserver_Tests.swift index 87c27e1846c..c94c28b265a 100644 --- a/Tests/StreamChatTests/Workers/EventObservers/EventObserver_Tests.swift +++ b/Tests/StreamChatTests/Workers/EventObservers/EventObserver_Tests.swift @@ -34,7 +34,7 @@ final class EventObserver_Tests: XCTestCase { // MARK: - Calling back tests func test_callbackIsNotCalled_ifObserverIsDeallocated() { - var callbackExecutionCount = 0 + nonisolated(unsafe) var callbackExecutionCount = 0 // Create observer and count callback executions observer = .init( @@ -58,7 +58,7 @@ final class EventObserver_Tests: XCTestCase { } func test_callbackIsCalled_ifEventCastSucceeds() { - var receivedEvent: HealthCheckEvent? + nonisolated(unsafe) var receivedEvent: HealthCheckEvent? // Create observer and catch event coming to callback observer = EventObserver( @@ -75,7 +75,7 @@ final class EventObserver_Tests: XCTestCase { } func test_callbackIsNotCalled_ifEventCastFails() { - var receivedEvent: Event? + nonisolated(unsafe) var receivedEvent: Event? // Create observer and catch event coming to callback observer = EventObserver( diff --git a/Tests/StreamChatTests/Workers/EventObservers/MemberEventObserver_Tests.swift b/Tests/StreamChatTests/Workers/EventObservers/MemberEventObserver_Tests.swift index 4125cc3254b..cf10ffbd35d 100644 --- a/Tests/StreamChatTests/Workers/EventObservers/MemberEventObserver_Tests.swift +++ b/Tests/StreamChatTests/Workers/EventObservers/MemberEventObserver_Tests.swift @@ -29,7 +29,7 @@ final class MemberEventObserver_Tests: XCTestCase { let memberEvent = TestMemberEvent.unique let otherEvent = OtherEvent() - var receivedEvents: [MemberEvent] = [] + nonisolated(unsafe) var receivedEvents: [MemberEvent] = [] observer = MemberEventObserver( notificationCenter: eventNotificationCenter, callback: { receivedEvents.append($0) } @@ -49,7 +49,7 @@ final class MemberEventObserver_Tests: XCTestCase { let matchingMemberEvent = TestMemberEvent(cid: channelId, memberUserId: .unique) let otherMemberEvent = TestMemberEvent.unique - var receivedEvents: [MemberEvent] = [] + nonisolated(unsafe) var receivedEvents: [MemberEvent] = [] observer = MemberEventObserver( notificationCenter: eventNotificationCenter, cid: channelId, diff --git a/Tests/StreamChatTests/Workers/EventSender_Tests.swift b/Tests/StreamChatTests/Workers/EventSender_Tests.swift index 7690bcb4886..9b2067078d9 100644 --- a/Tests/StreamChatTests/Workers/EventSender_Tests.swift +++ b/Tests/StreamChatTests/Workers/EventSender_Tests.swift @@ -45,7 +45,7 @@ final class EventSender_Tests: XCTestCase { func test_sendEvent_propagatesSuccessfulResponse() { // Simulate `sendEvent` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false sender.sendEvent(IdeaEventPayload.unique, to: .unique) { error in XCTAssertNil(error) completionCalled = true @@ -63,7 +63,7 @@ final class EventSender_Tests: XCTestCase { func test_sendEvent_propagatesError() { // Simulate `sendEvent` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? sender.sendEvent(IdeaEventPayload.unique, to: .unique) { error in completionCalledError = error } diff --git a/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift b/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift index a9659877729..d97157ac804 100644 --- a/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift @@ -418,7 +418,7 @@ final class MessageUpdater_Tests: XCTestCase { try database.createCurrentUser() // Simulate `deleteMessage(messageId:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.deleteMessage(messageId: messageId, hard: false) { completionCalledError = $0 } @@ -457,7 +457,7 @@ final class MessageUpdater_Tests: XCTestCase { try database.createCurrentUser() // Simulate `deleteMessage(messageId:)` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? let expectation = self.expectation(description: "Delete message completion") messageUpdater.deleteMessage(messageId: messageId, hard: false) { completionCalledError = $0 @@ -819,7 +819,7 @@ final class MessageUpdater_Tests: XCTestCase { messageRepository.getMessageResult = .success(message) // Simulate `getMessage(cid:, messageId:)` call - var result: Result! + nonisolated(unsafe) var result: Result! let expectation = self.expectation(description: "getMessage completes") messageUpdater.getMessage(cid: cid, messageId: messageId) { result = $0 @@ -837,7 +837,7 @@ final class MessageUpdater_Tests: XCTestCase { messageRepository.getMessageResult = .failure(error) // Simulate `getMessage(cid:, messageId:)` call - var result: Result! + nonisolated(unsafe) var result: Result! let expectation = self.expectation(description: "getMessage completes") messageUpdater.getMessage(cid: cid, messageId: messageId) { result = $0 @@ -1006,7 +1006,7 @@ final class MessageUpdater_Tests: XCTestCase { func test_loadReplies_propagatesRequestError() { // Simulate `loadReplies` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.loadReplies(cid: .unique, messageId: .unique, pagination: .init(pageSize: 25), paginationStateHandler: paginationStateHandler) { completionCalledError = $0.error } @@ -1033,7 +1033,7 @@ final class MessageUpdater_Tests: XCTestCase { database.write_errorResponse = testError // Simulate `loadReplies` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.loadReplies(cid: cid, messageId: .unique, pagination: .init(pageSize: 25), paginationStateHandler: paginationStateHandler) { completionCalledError = $0.error } @@ -1057,7 +1057,7 @@ final class MessageUpdater_Tests: XCTestCase { try database.createChannel(cid: cid) // Simulate `loadReplies` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false messageUpdater.loadReplies(cid: cid, messageId: .unique, pagination: .init(pageSize: 25), paginationStateHandler: paginationStateHandler) { _ in completionCalled = true } @@ -1139,7 +1139,7 @@ final class MessageUpdater_Tests: XCTestCase { } func test_loadReactions_propagatesRequestError() { - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.loadReactions(cid: .unique, messageId: .unique, pagination: .init(pageSize: 25)) { completionCalledError = $0.error } @@ -1166,7 +1166,7 @@ final class MessageUpdater_Tests: XCTestCase { let testError = TestError() database.write_errorResponse = testError - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.loadReactions(cid: cid, messageId: .unique, pagination: .init(pageSize: 25)) { completionCalledError = $0.error } @@ -1199,7 +1199,7 @@ final class MessageUpdater_Tests: XCTestCase { ] ) - var completionCalled = false + nonisolated(unsafe) var completionCalled = false messageUpdater.loadReactions(cid: cid, messageId: messageId, pagination: .init(pageSize: 25)) { result in XCTAssertEqual(try? result.get().count, reactionsPayload.reactions.count) completionCalled = true @@ -1277,7 +1277,7 @@ final class MessageUpdater_Tests: XCTestCase { } // Simulate `unflagMessage` call. - var unflagCompletionCalled = false + nonisolated(unsafe) var unflagCompletionCalled = false messageUpdater.flagMessage(false, with: messageId, in: cid) { error in XCTAssertNil(error) unflagCompletionCalled = true @@ -1310,7 +1310,7 @@ final class MessageUpdater_Tests: XCTestCase { messageRepository.getMessageResult = .failure(networkError) // Simulate `flagMessage` call and catch the error. - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.flagMessage(true, with: messageId, in: cid, reason: reason) { completionCalledError = $0 } @@ -1328,7 +1328,7 @@ final class MessageUpdater_Tests: XCTestCase { try database.createMessage(id: messageId) // Simulate `flagMessage` call and catch the error. - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.flagMessage(true, with: messageId, in: cid, reason: reason) { completionCalledError = $0 } @@ -1359,7 +1359,7 @@ final class MessageUpdater_Tests: XCTestCase { database.write_errorResponse = databaseError // Simulate `flagMessage` call and catch the error. - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.flagMessage(true, with: messageId, in: cid, reason: reason) { completionCalledError = $0 } @@ -1389,7 +1389,7 @@ final class MessageUpdater_Tests: XCTestCase { try database.createMessage(id: messageId) // Simulate `flagMessage` call and catch the error. - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.flagMessage(true, with: messageId, in: cid, reason: reason) { completionCalledError = $0 } @@ -2493,8 +2493,8 @@ final class MessageUpdater_Tests: XCTestCase { ) // Simulate `dispatchEphemeralMessageAction` - var completionCalledError: Error? - var completionCalled = false + nonisolated(unsafe) var completionCalledError: Error? + nonisolated(unsafe) var completionCalled = false messageUpdater.dispatchEphemeralMessageAction( cid: cid, messageId: messageId, @@ -2555,8 +2555,8 @@ final class MessageUpdater_Tests: XCTestCase { ) // Simulate `dispatchEphemeralMessageAction` - var completionCalledError: Error? - var completionCalled = false + nonisolated(unsafe) var completionCalledError: Error? + nonisolated(unsafe) var completionCalled = false messageUpdater.dispatchEphemeralMessageAction( cid: cid, messageId: messageId, @@ -2699,7 +2699,7 @@ final class MessageUpdater_Tests: XCTestCase { ) // Simulate `dispatchEphemeralMessageAction` - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? messageUpdater.dispatchEphemeralMessageAction( cid: cid, messageId: messageId, @@ -2754,7 +2754,7 @@ final class MessageUpdater_Tests: XCTestCase { try database.createChannel(cid: cid) // Make translate call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false messageUpdater.translate(messageId: messageId, to: language) { result in completionCalled = true XCTAssertNil(result.error) @@ -2781,7 +2781,7 @@ final class MessageUpdater_Tests: XCTestCase { let language = TranslationLanguage.allCases.randomElement()! // Make translate call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false let testError = TestError() messageUpdater.translate(messageId: messageId, to: language) { result in completionCalled = true @@ -2808,7 +2808,7 @@ final class MessageUpdater_Tests: XCTestCase { database.write_errorResponse = testError // Make translate call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false messageUpdater.translate(messageId: messageId, to: language) { result in completionCalled = true XCTAssertEqual(result.error as? TestError, testError) @@ -3105,7 +3105,7 @@ extension MessageUpdater_Tests { let attachmentId: AttachmentId = .init(cid: cid, messageId: messageId, index: 0) try database.createChannel(cid: cid, withMessages: false) try database.createMessage(id: messageId, cid: cid) - var result: ChatMessageAttachment! + nonisolated(unsafe) var result: ChatMessageAttachment! try database.writeSynchronously { session in let anyPayload = AnyAttachmentPayload(type: attachment.type, payload: attachment.payload, localFileURL: nil) let dto = try session.createNewAttachment(attachment: anyPayload, id: attachmentId) diff --git a/Tests/StreamChatTests/Workers/TypingEventSender_Tests.swift b/Tests/StreamChatTests/Workers/TypingEventSender_Tests.swift index 15c2a5e1d37..c2099cb7da6 100644 --- a/Tests/StreamChatTests/Workers/TypingEventSender_Tests.swift +++ b/Tests/StreamChatTests/Workers/TypingEventSender_Tests.swift @@ -21,7 +21,7 @@ final class TypingEventsSender_Tests: XCTestCase { database = DatabaseContainer_Spy() time = VirtualTime() - VirtualTimeTimer.time = time + VirtualTimeTimer.time.value = time eventSender = TypingEventsSender(database: database, apiClient: apiClient) eventSender.timer = VirtualTimeTimer.self diff --git a/Tests/StreamChatTests/Workers/UserListUpdater_Tests.swift b/Tests/StreamChatTests/Workers/UserListUpdater_Tests.swift index b0a77e5f714..ccbf9a4fc25 100644 --- a/Tests/StreamChatTests/Workers/UserListUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/UserListUpdater_Tests.swift @@ -78,7 +78,7 @@ final class UserListUpdater_Tests: XCTestCase { func test_update_errorResponse_isPropagatedToCompletion() { // Simulate `update` call let query = UserListQuery(filter: .equal(.id, to: "Luke")) - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? listUpdater.update(userListQuery: query, completion: { completionCalledError = $0.error }) // Simualte API response with failure @@ -114,7 +114,7 @@ final class UserListUpdater_Tests: XCTestCase { func test_mergePolicy_takesAffect() throws { // Simulate `update` call let query = UserListQuery(filter: .equal(.id, to: "Luke")) - var completionCalled = expectation(description: "completion called") + let completionCalled = expectation(description: "completion called") listUpdater.update(userListQuery: query) { _ in completionCalled.fulfill() } // Simulate API response with user data @@ -130,15 +130,15 @@ final class UserListUpdater_Tests: XCTestCase { // Simulate consequent `update` call with new users and `.merge` policy // We don't pass the `policy` argument since we expect it's `merge` by default - completionCalled = expectation(description: "completion called") - listUpdater.update(userListQuery: query) { _ in completionCalled.fulfill() } + let completionCalled2 = expectation(description: "completion called") + listUpdater.update(userListQuery: query) { _ in completionCalled2.fulfill() } // Simulate API response with user data let newUserId = UserId.unique let newPayload = UserListPayload(users: [.dummy(userId: newUserId)]) apiClient.test_simulateResponse(.success(newPayload)) - wait(for: [completionCalled], timeout: defaultTimeout) + wait(for: [completionCalled2], timeout: defaultTimeout) // Assert new user is inserted into DB var newUser: ChatUser? { try? self.user(with: newUserId) } @@ -167,7 +167,7 @@ final class UserListUpdater_Tests: XCTestCase { // Simulate `update` call // This call doesn't need `policy` argument specified since // it's the first call for this query, hence there's no data to `replace` or `merge` to - var completionCalled = expectation(description: "completion called") + let completionCalled = expectation(description: "completion called") listUpdater.update(userListQuery: query) { _ in completionCalled.fulfill() } // Simulate API response with user data @@ -185,15 +185,15 @@ final class UserListUpdater_Tests: XCTestCase { AssertAsync.willBeTrue(user != nil) // Simulate consequent `update` call with new users and `.replace` policy - completionCalled = expectation(description: "completion called") - listUpdater.update(userListQuery: query, policy: .replace) { _ in completionCalled.fulfill() } + let completionCalled2 = expectation(description: "completion called") + listUpdater.update(userListQuery: query, policy: .replace) { _ in completionCalled2.fulfill() } // Simulate API response with user data let newUserId = UserId.unique let newPayload = UserListPayload(users: [.dummy(userId: newUserId)]) apiClient.test_simulateResponse(.success(newPayload)) - wait(for: [completionCalled], timeout: defaultTimeout) + wait(for: [completionCalled2], timeout: defaultTimeout) // Assert new user is inserted into DB AssertAsync.willBeTrue((try? self.user(with: newUserId)) != nil) @@ -215,14 +215,15 @@ final class UserListUpdater_Tests: XCTestCase { // Simulate `update` call let query = UserListQuery(filter: .equal(.id, to: "Luke")) - var completionCalled = false + nonisolated(unsafe) var completionCalled = false listUpdater.update(userListQuery: query, completion: { _ in // At this point, DB write should have completed // Assert the data is stored in the DB // We call this block in `main` queue since we need to access `viewContext` + nonisolated(unsafe) let unsafeSelf = self DispatchQueue.main.sync { - let user: ChatUser? = try? self.user(with: dummyUserId) + let user: ChatUser? = try? unsafeSelf.user(with: dummyUserId) XCTAssert(user != nil) @@ -252,7 +253,7 @@ final class UserListUpdater_Tests: XCTestCase { func test_fetch_whenSuccess_payloadIsPropagatedToCompletion() { // Simulate `fetch` call let query = UserListQuery(filter: .equal(.id, to: "Luke")) - var userListPayload: UserListPayload? + nonisolated(unsafe) var userListPayload: UserListPayload? listUpdater.fetch(userListQuery: query, completion: { result in XCTAssertNil(result.error) userListPayload = try? result.get() @@ -271,7 +272,7 @@ final class UserListUpdater_Tests: XCTestCase { func test_fetch_whenFailure_errorIsPropagatedToCompletion() { // Simulate `fetch` call let query = UserListQuery(filter: .equal(.id, to: "Luke")) - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? listUpdater.fetch(userListQuery: query, completion: { completionCalledError = $0.error }) // Simualte API response with failure diff --git a/Tests/StreamChatTests/Workers/UserUpdater_Tests.swift b/Tests/StreamChatTests/Workers/UserUpdater_Tests.swift index 30b16e67392..a39ee3074bb 100644 --- a/Tests/StreamChatTests/Workers/UserUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/UserUpdater_Tests.swift @@ -52,7 +52,7 @@ final class UserUpdater_Tests: XCTestCase { func test_muteUser_propagatesSuccessfulResponse() { // Simulate `muteUser` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false userUpdater.muteUser(.unique) { error in XCTAssertNil(error) completionCalled = true @@ -70,7 +70,7 @@ final class UserUpdater_Tests: XCTestCase { func test_muteUser_propagatesError() { // Simulate `muteUser` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? userUpdater.muteUser(.unique) { completionCalledError = $0 } @@ -97,7 +97,7 @@ final class UserUpdater_Tests: XCTestCase { func test_unmuteUser_propagatesSuccessfulResponse() { // Simulate `muteUser` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false userUpdater.unmuteUser(.unique) { error in XCTAssertNil(error) completionCalled = true @@ -115,7 +115,7 @@ final class UserUpdater_Tests: XCTestCase { func test_unmuteUser_propagatesError() { // Simulate `muteUser` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? userUpdater.unmuteUser(.unique) { completionCalledError = $0 } @@ -143,7 +143,7 @@ final class UserUpdater_Tests: XCTestCase { func test_loadUser_propogatesNetworkError() { // Simulate `loadUser(_ userId:)` call. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? userUpdater.loadUser(.unique) { completionError = $0 } @@ -158,7 +158,7 @@ final class UserUpdater_Tests: XCTestCase { func test_loadUser_propogatesUserDoesNotExistError() { // Simulate `loadUser(_ userId:)` call. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? userUpdater.loadUser(.unique) { completionError = $0 } @@ -175,7 +175,7 @@ final class UserUpdater_Tests: XCTestCase { let userId: UserId = .unique // Simulate `loadUser(_ userId:)` call. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? userUpdater.loadUser(userId) { completionError = $0 } @@ -206,7 +206,7 @@ final class UserUpdater_Tests: XCTestCase { database.write_errorResponse = databaseError // Simulate `loadUser(_ userId:)` call. - var completionError: Error? + nonisolated(unsafe) var completionError: Error? userUpdater.loadUser(.unique) { completionError = $0 } @@ -222,7 +222,7 @@ final class UserUpdater_Tests: XCTestCase { func test_loadUser_savesReceivedUserToDatabase() { // Simulate `loadUser(_ userId:)` call. - var completionIsCalled = false + nonisolated(unsafe) var completionIsCalled = false userUpdater.loadUser(.unique) { _ in completionIsCalled = true } @@ -271,7 +271,7 @@ final class UserUpdater_Tests: XCTestCase { try database.createCurrentUser(id: currentUserId) // Simulate `flagUser` call. - var flagCompletionCalled = false + nonisolated(unsafe) var flagCompletionCalled = false userUpdater.flagUser(true, with: flaggedUserId, reason: nil, extraData: nil) { error in XCTAssertNil(error) flagCompletionCalled = true @@ -300,7 +300,7 @@ final class UserUpdater_Tests: XCTestCase { } // Simulate `unflagUser` call. - var unflagCompletionCalled = false + nonisolated(unsafe) var unflagCompletionCalled = false userUpdater.flagUser(false, with: flaggedUserId, reason: nil, extraData: nil) { error in XCTAssertNil(error) unflagCompletionCalled = true @@ -318,7 +318,7 @@ final class UserUpdater_Tests: XCTestCase { func test_flagUser_propagatesNetworkError() { // Simulate `flagUser` call. - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? userUpdater.flagUser(true, with: .unique, reason: nil, extraData: nil) { completionCalledError = $0 } @@ -337,7 +337,7 @@ final class UserUpdater_Tests: XCTestCase { database.write_errorResponse = databaseError // Simulate `flagUser` call. - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? userUpdater.flagUser(true, with: .unique, reason: nil, extraData: nil) { completionCalledError = $0 } @@ -367,7 +367,7 @@ final class UserUpdater_Tests: XCTestCase { func test_blockUser_propagatesSuccessfulResponse() { // Simulate `blockUser` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false userUpdater.blockUser(.unique) { error in XCTAssertNil(error) completionCalled = true @@ -386,7 +386,7 @@ final class UserUpdater_Tests: XCTestCase { func test_blockUser_propagatesError() { // Simulate `blockUser` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? userUpdater.blockUser(.unique) { completionCalledError = $0 } @@ -413,7 +413,7 @@ final class UserUpdater_Tests: XCTestCase { func test_unblockUser_propagatesSuccessfulResponse() { // Simulate `blockUser` call - var completionCalled = false + nonisolated(unsafe) var completionCalled = false userUpdater.unblockUser(.unique) { error in XCTAssertNil(error) completionCalled = true @@ -431,7 +431,7 @@ final class UserUpdater_Tests: XCTestCase { func test_unblockUser_propagatesError() { // Simulate `blockUser` call - var completionCalledError: Error? + nonisolated(unsafe) var completionCalledError: Error? userUpdater.unblockUser(.unique) { completionCalledError = $0 } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c108679650d..53385600271 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -3,6 +3,7 @@ skip_docs require 'json' require 'net/http' +require 'xcodeproj' import 'Sonarfile' import 'Allurefile'