From 504a3498485d145c628b3bdbf671ca1a7eeb8edc Mon Sep 17 00:00:00 2001
From: Toomas Vahter
Date: Fri, 3 Jan 2025 14:45:15 +0200
Subject: [PATCH 1/2] Remove swift version checks and set the required tools
version to 5.7 (#3547)
---
Package.swift | 4 +--
README.md | 4 +--
StreamChat-XCFramework.podspec | 2 +-
StreamChat.podspec | 2 +-
StreamChatUI-XCFramework.podspec | 2 +-
StreamChatUI.podspec | 2 +-
.../StateLayer/ChannelList_Tests.swift | 27 +++++--------------
.../StateLayer/Chat_Tests.swift | 4 +--
.../StateLayerDatabaseObserver_Tests.swift | 22 +++++++--------
.../StateLayer/UserSearch_Tests.swift | 4 +--
.../ChatMessage+Equatable_Tests.swift | 2 --
11 files changed, 28 insertions(+), 47 deletions(-)
diff --git a/Package.swift b/Package.swift
index d959fcd4415..17c6eec1c8a 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.6
+// swift-tools-version:5.7
import Foundation
import PackageDescription
@@ -56,8 +56,6 @@ let package = Package(
]
)
-#if swift(>=5.6)
package.dependencies.append(
.package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.0.0")
)
-#endif
diff --git a/README.md b/README.md
index 9166a4624f5..e6c9c82f6e2 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-
+
@@ -39,7 +39,7 @@ The **StreamChatSwiftUI SDK** is our UI SDK for SwiftUI components. If your appl
- **Familiar behavior**: The UI elements are good platform citizens and behave like native elements; they respect `tintColor`, `layoutMargins`, light/dark mode, dynamic font sizes, etc.
- **Swift native API:** Uses Swift's powerful language features to make the SDK usage easy and type-safe.
- `UIKit` and `SwiftUI` SDKs use native patterns and paradigms from respective UI frameworks: The API follows the design of native system SDKs. It makes integration with your existing code easy and familiar.
- - `UIKit` SDK is part of this repository whereas `SwiftUI` SDK is available [here](https://github.com/GetStream/stream-chat-swiftui).
+ - `UIKit` SDK is part of this repository whereas `SwiftUI` SDK is available [here](https://github.com/GetStream/stream-chat-swiftui).
- **First-class support for `Combine` and `Structured Concurrency`**: Refer to our getting started guides for [Combine](https://getstream.io/chat/docs/sdk/ios/combine/) and [Structured Concurrency](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
- **Fully open-source implementation:** You have access to the complete source code of the SDK here on GitHub.
- **Supports iOS 13+:** We proudly support older versions of iOS, so your app can stay available to almost everyone.
diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec
index 18235e95b3f..329896cf55a 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.6'
+ spec.swift_version = '5.7'
spec.ios.deployment_target = '13.0'
spec.requires_arc = true
diff --git a/StreamChat.podspec b/StreamChat.podspec
index f747a731c19..62cc71e612b 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.6'
+ spec.swift_version = '5.7'
spec.ios.deployment_target = '13.0'
spec.osx.deployment_target = '11.0'
spec.requires_arc = true
diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec
index 9bf3e2a0bef..ae645a20990 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.6'
+ spec.swift_version = '5.7'
spec.platform = :ios, "13.0"
spec.requires_arc = true
diff --git a/StreamChatUI.podspec b/StreamChatUI.podspec
index 2f1ee75243d..3d7c0bd19ec 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.6'
+ spec.swift_version = '5.7'
spec.platform = :ios, "13.0"
spec.requires_arc = true
diff --git a/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift b/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift
index 116e82ef80b..79a9a1af8f3 100644
--- a/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift
+++ b/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift
@@ -237,7 +237,7 @@ final class ChannelList_Tests: XCTestCase {
try await env.client.mockDatabaseContainer.write { session in
session.saveChannelList(payload: incomingChannelListPayload, query: self.channelList.query)
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
cancellable.cancel()
}
@@ -266,7 +266,7 @@ final class ChannelList_Tests: XCTestCase {
try await env.client.mockDatabaseContainer.write { session in
session.saveChannelList(payload: incomingChannelListPayload, query: self.channelList.query)
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
cancellable.cancel()
}
@@ -303,7 +303,7 @@ final class ChannelList_Tests: XCTestCase {
try await env.client.mockDatabaseContainer.write { session in
session.saveChannelList(payload: incomingChannelListPayload, query: self.channelList.query)
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
cancellable.cancel()
}
@@ -339,7 +339,7 @@ final class ChannelList_Tests: XCTestCase {
try await env.client.mockDatabaseContainer.write { session in
session.saveChannelList(payload: incomingChannelListPayload, query: self.channelList.query)
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
cancellable.cancel()
}
@@ -381,7 +381,7 @@ final class ChannelList_Tests: XCTestCase {
let eventExpectation = XCTestExpectation(description: "Event processed")
env.client.eventNotificationCenter.process([event], completion: { eventExpectation.fulfill() })
- await fulfillmentCompatibility(of: [eventExpectation, stateExpectation], timeout: defaultTimeout, enforceOrder: true)
+ await fulfillment(of: [eventExpectation, stateExpectation], timeout: defaultTimeout, enforceOrder: true)
cancellable.cancel()
}
@@ -414,7 +414,7 @@ final class ChannelList_Tests: XCTestCase {
)
let eventExpectation = XCTestExpectation(description: "Event processed")
env.client.eventNotificationCenter.process([event], completion: { eventExpectation.fulfill() })
- await fulfillmentCompatibility(of: [eventExpectation], timeout: defaultTimeout, enforceOrder: true)
+ await fulfillment(of: [eventExpectation], timeout: defaultTimeout, enforceOrder: true)
cancellable.cancel()
}
@@ -539,18 +539,3 @@ extension ChannelList_Tests {
}
}
}
-
-extension XCTestCase {
- func fulfillmentCompatibility(of expectations: [XCTestExpectation], timeout seconds: TimeInterval, enforceOrder enforceOrderOfFulfillment: Bool = false) async {
- #if swift(>=5.8)
- await fulfillment(of: expectations, timeout: seconds, enforceOrder: enforceOrderOfFulfillment)
- #else
- await withCheckedContinuation { continuation in
- Thread.detachNewThread { [self] in
- wait(for: expectations, timeout: seconds, enforceOrder: enforceOrderOfFulfillment)
- continuation.resume()
- }
- }
- #endif
- }
-}
diff --git a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift
index b0b8ee1649c..0c3abf60a9e 100644
--- a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift
+++ b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift
@@ -662,7 +662,7 @@ final class Chat_Tests: XCTestCase {
messageId: apiResponse.message.id
)
- await fulfillmentCompatibility(of: [notificationExpectation], timeout: defaultTimeout)
+ await fulfillment(of: [notificationExpectation], timeout: defaultTimeout)
XCTAssertEqual(text, message.text)
await XCTAssertEqual(1, chat.state.messages.count)
@@ -1304,7 +1304,7 @@ final class Chat_Tests: XCTestCase {
)
XCTAssertEqual(apiResponse.message.id, replyMessage.id)
- await fulfillmentCompatibility(of: [notificationExpectation], timeout: defaultTimeout)
+ await fulfillment(of: [notificationExpectation], timeout: defaultTimeout)
let messageState = try await chat.messageState(for: lastMessageId)
await XCTAssertEqual(lastMessageId, messageState.message.id)
diff --git a/Tests/StreamChatTests/StateLayer/StateLayerDatabaseObserver_Tests.swift b/Tests/StreamChatTests/StateLayer/StateLayerDatabaseObserver_Tests.swift
index 53842d40ef0..1e3b77d4d53 100644
--- a/Tests/StreamChatTests/StateLayer/StateLayerDatabaseObserver_Tests.swift
+++ b/Tests/StreamChatTests/StateLayer/StateLayerDatabaseObserver_Tests.swift
@@ -42,7 +42,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try await waitForDuplicateCallbacks()
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
XCTAssertEqual("first", observer.item?.name)
XCTAssertEqual(1, changeCount)
@@ -70,7 +70,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try await waitForDuplicateCallbacks()
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
XCTAssertEqual("second", observer.item?.name)
XCTAssertEqual(1, changeCount)
@@ -98,7 +98,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try await waitForDuplicateCallbacks()
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
XCTAssertEqual("second", observer.item?.name)
XCTAssertEqual("team2", observer.item?.team)
@@ -128,7 +128,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try await waitForDuplicateCallbacks()
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
XCTAssertEqual(8, observer.items.count)
let expectedIds = (firstPayload.messages + secondPayload.messages).map(\.id)
@@ -161,7 +161,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try await waitForDuplicateCallbacks()
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
XCTAssertEqual(3, observer.items.count)
XCTAssertEqual(secondPayload.messages.map(\.id), observer.items.map(\.id))
@@ -195,7 +195,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try await waitForDuplicateCallbacks()
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
XCTAssertEqual(11, observer.items.count)
let expectedIds = (firstPayload.messages + secondPayload.messages + thirdPayload.messages).map(\.id)
@@ -254,7 +254,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try session.saveChannel(payload: channelPayload, query: query, cache: nil)
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
// 4 are reused, 1 is created
XCTAssertEqual(6, itemCreatorCounter)
@@ -279,7 +279,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try session.saveChannel(payload: secondPayload)
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
// 5 are reused, 5 are created
XCTAssertEqual(15, messageItemCreatorCounter)
@@ -329,7 +329,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try session.saveReaction(payload: makePayload(1).reactions[0], query: query, cache: nil)
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
// 4 are reused, 1 is created
XCTAssertEqual(6, itemCreatorCounter)
@@ -365,7 +365,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try session.saveThread(payload: makePayload(1).threads[0], cache: nil)
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
// 4 are reused, 1 is created
XCTAssertEqual(6, itemCreatorCounter)
@@ -405,7 +405,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase {
try session.saveUser(payload: makePayload(1).users[0])
}
- await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout)
+ await fulfillment(of: [expectation], timeout: defaultTimeout)
// 4 are reused, 1 is created
XCTAssertEqual(6, itemCreatorCounter)
diff --git a/Tests/StreamChatTests/StateLayer/UserSearch_Tests.swift b/Tests/StreamChatTests/StateLayer/UserSearch_Tests.swift
index b73b0a2d4f6..4ac4f1383f8 100644
--- a/Tests/StreamChatTests/StateLayer/UserSearch_Tests.swift
+++ b/Tests/StreamChatTests/StateLayer/UserSearch_Tests.swift
@@ -75,7 +75,7 @@ final class UserSearch_Tests: XCTestCase {
expectation1.fulfill()
}
- await fulfillmentCompatibility(of: [expectation1], timeout: defaultTimeout)
+ await fulfillment(of: [expectation1], timeout: defaultTimeout)
// Search for "name"
async let result2 = try await userSearch.search(term: "name")
@@ -84,7 +84,7 @@ final class UserSearch_Tests: XCTestCase {
expectation2.fulfill()
}
- await fulfillmentCompatibility(of: [expectation2], timeout: defaultTimeout)
+ await fulfillment(of: [expectation2], timeout: defaultTimeout)
XCTAssertEqual(2, env.userListUpdaterMock.fetch_completions.count)
diff --git a/Tests/StreamChatUITests/Extensions/ChatMessage+Equatable_Tests.swift b/Tests/StreamChatUITests/Extensions/ChatMessage+Equatable_Tests.swift
index 18f1b791bab..9dd996e00ed 100644
--- a/Tests/StreamChatUITests/Extensions/ChatMessage+Equatable_Tests.swift
+++ b/Tests/StreamChatUITests/Extensions/ChatMessage+Equatable_Tests.swift
@@ -6,7 +6,6 @@
@testable import StreamChatTestTools
import XCTest
-#if swift(>=5.7)
@available(iOS 16.0, *)
final class ChatMessage_Equatable_Tests: XCTestCase {
var database: DatabaseContainer!
@@ -245,4 +244,3 @@ extension Collection {
}
}
}
-#endif
From 0485e9908f853cc6b17da692bfe924edda624e76 Mon Sep 17 00:00:00 2001
From: Toomas Vahter
Date: Fri, 3 Jan 2025 15:39:46 +0200
Subject: [PATCH 2/2] Remove SyncRepository V1 implementation (#3549)
---
.../Config/StreamRuntimeCheck.swift | 5 -
.../ChannelListController.swift | 22 --
.../StreamChat/Database/DTOs/ChannelDTO.swift | 12 -
.../StreamChat/Database/DatabaseSession.swift | 3 -
.../Repositories/SyncOperations.swift | 109 --------
.../Repositories/SyncRepository.swift | 101 +-------
.../Workers/ChannelListUpdater.swift | 68 +----
.../ChatChannelListController_Mock.swift | 10 -
.../Database/DatabaseSession_Mock.swift | 4 -
.../Spy/ChannelListUpdater_Spy.swift | 14 +-
.../ChannelListController_Tests.swift | 52 +---
.../Repositories/SyncOperations_Tests.swift | 192 --------------
.../Repositories/SyncRepository_Tests.swift | 236 +++---------------
.../Workers/ChannelListUpdater_Tests.swift | 197 +++------------
14 files changed, 92 insertions(+), 933 deletions(-)
diff --git a/Sources/StreamChat/Config/StreamRuntimeCheck.swift b/Sources/StreamChat/Config/StreamRuntimeCheck.swift
index 9c80676ea2a..12f2a3873c4 100644
--- a/Sources/StreamChat/Config/StreamRuntimeCheck.swift
+++ b/Sources/StreamChat/Config/StreamRuntimeCheck.swift
@@ -32,11 +32,6 @@ public enum StreamRuntimeCheck {
/// Enables reusing unchanged converted items in database observers.
public static var _isDatabaseObserverItemReusingEnabled = true
- /// For *internal use* only
- ///
- /// Uses version 2 for offline state sync.
- public static var _isSyncV2Enabled = true
-
/// For *internal use* only
///
/// Core Data prefetches data used for creating immutable model objects (faulting is disabled).
diff --git a/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift b/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
index fec5446a3cd..ca743167295 100644
--- a/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
+++ b/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
@@ -197,28 +197,6 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
let channelCount = channelListObserver.items.count
worker.refreshLoadedChannels(for: query, channelCount: channelCount, completion: completion)
}
-
- func resetQuery(
- watchedAndSynchedChannelIds: Set,
- synchedChannelIds: Set,
- completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>) -> Void
- ) {
- let pageSize = query.pagination.pageSize
- worker.resetChannelsQuery(
- for: query,
- pageSize: pageSize,
- watchedAndSynchedChannelIds: watchedAndSynchedChannelIds,
- synchedChannelIds: synchedChannelIds
- ) { [weak self] result in
- switch result {
- case let .success((newChannels, unwantedCids)):
- self?.hasLoadedAllPreviousChannels = newChannels.count < pageSize
- completion(.success((newChannels, unwantedCids)))
- case let .failure(error):
- completion(.failure(error))
- }
- }
- }
// MARK: - Helpers
diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
index b57d5583c35..9ed3f411b44 100644
--- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
@@ -373,18 +373,6 @@ extension NSManagedObjectContext {
delete(dto)
}
- func cleanChannels(cids: Set) {
- let channels = ChannelDTO.load(cids: Array(cids), context: self)
- for channelDTO in channels {
- channelDTO.resetEphemeralValues()
- channelDTO.messages.removeAll()
- channelDTO.members.removeAll()
- channelDTO.pinnedMessages.removeAll()
- channelDTO.reads.removeAll()
- channelDTO.oldestMessageAt = nil
- }
- }
-
func removeChannels(cids: Set) {
let channels = ChannelDTO.load(cids: Array(cids), context: self)
channels.forEach(delete)
diff --git a/Sources/StreamChat/Database/DatabaseSession.swift b/Sources/StreamChat/Database/DatabaseSession.swift
index 438f49d0396..5a19d0f44f0 100644
--- a/Sources/StreamChat/Database/DatabaseSession.swift
+++ b/Sources/StreamChat/Database/DatabaseSession.swift
@@ -302,9 +302,6 @@ protocol ChannelDatabaseSession {
/// Removes channel list query from database.
func delete(query: ChannelListQuery)
- /// Cleans a list of channels based on their id
- func cleanChannels(cids: Set)
-
/// Removes a list of channels based on their id
func removeChannels(cids: Set)
}
diff --git a/Sources/StreamChat/Repositories/SyncOperations.swift b/Sources/StreamChat/Repositories/SyncOperations.swift
index 2b3b0a93dae..bead0bbac74 100644
--- a/Sources/StreamChat/Repositories/SyncOperations.swift
+++ b/Sources/StreamChat/Repositories/SyncOperations.swift
@@ -107,25 +107,6 @@ final class RefreshChannelListOperation: AsyncOperation, @unchecked Sendable {
}
}
-final class GetChannelIdsOperation: AsyncOperation, @unchecked Sendable {
- init(database: DatabaseContainer, context: SyncContext, activeChannelIds: [ChannelId]) {
- super.init(maxRetries: syncOperationsMaximumRetries) { [weak database] _, done in
- guard let database = database else {
- done(.continue)
- return
- }
- database.backgroundReadOnlyContext.perform {
- let cids = database.backgroundReadOnlyContext.loadAllChannelListQueries()
- .flatMap(\.channels)
- .compactMap { try? ChannelId(cid: $0.cid) }
- log.info("0. Retrieved channels from existing queries from DB. Count \(cids.count)", subsystems: .offlineSupport)
- context.localChannelIds = Set(cids + activeChannelIds)
- done(.continue)
- }
- }
- }
-}
-
final class SyncEventsOperation: AsyncOperation, @unchecked Sendable {
init(syncRepository: SyncRepository, context: SyncContext, recovery: Bool) {
super.init(maxRetries: syncOperationsMaximumRetries) { [weak syncRepository] _, done in
@@ -208,96 +189,6 @@ final class WatchChannelOperation: AsyncOperation, @unchecked Sendable {
}
}
-final class RefetchChannelListQueryOperation: AsyncOperation, @unchecked Sendable {
- init(controller: ChatChannelListController, context: SyncContext) {
- super.init(maxRetries: syncOperationsMaximumRetries) { [weak controller] _, done in
- guard let controller = controller, controller.canBeRecovered else {
- done(.continue)
- return
- }
-
- let query = controller.query
-
- log.info("3 & 4. Refetching channel lists queries & Cleaning up local message history", subsystems: .offlineSupport)
- controller.resetQuery(
- watchedAndSynchedChannelIds: context.watchedAndSynchedChannelIds,
- synchedChannelIds: context.synchedChannelIds
- ) { result in
- Self.handleResult(result, query: query, context: context, done: done)
- }
- }
- }
-
- init(query: ChannelListQuery, channelListUpdater: ChannelListUpdater, context: SyncContext) {
- super.init(maxRetries: syncOperationsMaximumRetries) { _, done in
- log.info("3 & 4. Refetching channel lists queries (step 2)", subsystems: .offlineSupport)
- channelListUpdater.resetChannelsQuery(
- for: query,
- pageSize: query.pagination.pageSize,
- watchedAndSynchedChannelIds: context.watchedAndSynchedChannelIds,
- synchedChannelIds: context.synchedChannelIds
- ) { result in
- Self.handleResult(result, query: query, context: context, done: done)
- }
- }
- }
-
- private static func handleResult(
- _ result: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), any Error>,
- query: ChannelListQuery,
- context: SyncContext,
- done: (AsyncOperation.Output) -> Void
- ) {
- switch result {
- case let .success((watchedChannels, unwantedCids)):
- log.info("Successfully refetched query for \(query.debugDescription)", subsystems: .offlineSupport)
- let queryChannelIds = watchedChannels.map(\.cid)
- context.watchedAndSynchedChannelIds.formUnion(queryChannelIds)
- context.unwantedChannelIds.formUnion(unwantedCids)
- done(.continue)
- case let .failure(error):
- log.error(
- "Failed refetching query for \(query.debugDescription): \(error)",
- subsystems: .offlineSupport
- )
- done(.retry)
- }
- }
-}
-
-final class DeleteUnwantedChannelsOperation: AsyncOperation, @unchecked Sendable {
- init(database: DatabaseContainer, context: SyncContext) {
- super.init(maxRetries: syncOperationsMaximumRetries) { [weak database] _, done in
- log.info("4. Clean up unwanted channels", subsystems: .offlineSupport)
-
- guard let database = database, !context.unwantedChannelIds.isEmpty else {
- done(.continue)
- return
- }
-
- // We are going to remove those channels that are not present in remote queries, and that have not
- // been watched.
- database.write { session in
- // We remove watchedAndSynched from unwantedChannels because it might happen that a channel marked
- // as unwanted in one query, might still be needed in another query (scenario where multiple queries
- // are active at the same time).
- let idsToRemove = context.unwantedChannelIds.subtracting(context.watchedAndSynchedChannelIds)
- session.removeChannels(cids: idsToRemove)
- } completion: { error in
- if let error = error {
- log.error(
- "Failed removing unwanted channels: \(error)",
- subsystems: .offlineSupport
- )
- done(.retry)
- } else {
- done(.continue)
- }
- }
- }
- }
-}
-
final class ExecutePendingOfflineActions: AsyncOperation, @unchecked Sendable {
init(offlineRequestsRepository: OfflineRequestsRepository) {
super.init(maxRetries: syncOperationsMaximumRetries) { [weak offlineRequestsRepository] _, done in
diff --git a/Sources/StreamChat/Repositories/SyncRepository.swift b/Sources/StreamChat/Repositories/SyncRepository.swift
index 7ca233158c1..2f146cce178 100644
--- a/Sources/StreamChat/Repositories/SyncRepository.swift
+++ b/Sources/StreamChat/Repositories/SyncRepository.swift
@@ -36,7 +36,6 @@ class SyncRepository {
private let database: DatabaseContainer
private let apiClient: APIClient
private let channelListUpdater: ChannelListUpdater
- var usesV2Sync = StreamRuntimeCheck._isSyncV2Enabled
let offlineRequestsRepository: OfflineRequestsRepository
let eventNotificationCenter: EventNotificationCenter
@@ -137,15 +136,11 @@ class SyncRepository {
}
return
}
- if self?.usesV2Sync == true {
- self?.syncLocalStateV2(lastSyncAt: lastSyncAt, completion: completion)
- } else {
- self?.syncLocalState(lastSyncAt: lastSyncAt, completion: completion)
- }
+ self?.syncLocalState(lastSyncAt: lastSyncAt, completion: completion)
}
}
- // MARK: - V2
+ // MARK: -
/// Runs offline tasks and updates the local state for channels
///
@@ -161,7 +156,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 syncLocalStateV2(lastSyncAt: Date, completion: @escaping () -> Void) {
+ private func syncLocalState(lastSyncAt: Date, completion: @escaping () -> Void) {
let context = SyncContext(lastSyncAt: lastSyncAt)
var operations: [Operation] = []
let start = CFAbsoluteTimeGetCurrent()
@@ -213,96 +208,6 @@ class SyncRepository {
}
operationQueue.addOperations(operations, waitUntilFinished: false)
}
-
- // MARK: - V1
-
- /// Syncs the local state with the server to make sure the local database is up to date.
- /// It features queuing, serialization and retries
- ///
- /// [Sync and watch channels](https://www.notion.so/2-Sync-and-watch-channels-ac44feb55de3482f8f0f99e100ca40c6)
- /// 1. Call `/sync` endpoint and get missing events for all locally existed channels
- /// 2. Start watching open channels
- /// 3. Refetch channel lists queries, link only what backend returns (the 1st page)
- /// 4. Clean up unwanted channels
- /// 5. Run offline actions requests
- ///
- /// - Parameter completion: A block that will get executed upon completion of the synchronization
- private func syncLocalState(lastSyncAt: Date, completion: @escaping () -> Void) {
- log.info("Starting to recover offline state", subsystems: .offlineSupport)
- let context = SyncContext(lastSyncAt: lastSyncAt)
- var operations: [Operation] = []
-
- // Enter recovery mode so no other requests are triggered.
- apiClient.enterRecoveryMode()
-
- // Run offline actions requests as the first thing
- if config.isLocalStorageEnabled {
- operations.append(ExecutePendingOfflineActions(offlineRequestsRepository: offlineRequestsRepository))
- }
-
- // Get the existing channelIds
- let activeChannelIds = activeChannelControllers.allObjects.compactMap(\.cid)
- operations.append(GetChannelIdsOperation(database: database, context: context, activeChannelIds: activeChannelIds))
-
- // 1. Call `/sync` endpoint and get missing events for all locally existed channels
- operations.append(SyncEventsOperation(syncRepository: self, context: context, recovery: true))
-
- // 2. Start watching open channels.
- let watchChannelOperations: [AsyncOperation] = activeChannelControllers.allObjects.map { controller in
- WatchChannelOperation(controller: controller, context: context, recovery: true)
- }
- operations.append(contentsOf: watchChannelOperations)
-
- // 3. Refetch channel lists queries, link only what backend returns (the 1st page)
- // We use `context.synchedChannelIds` to keep track of the channels that were synched both in the previous step and
- // after each ChannelListController recovery.
- let refetchChannelListQueryOperations: [AsyncOperation] = activeChannelListControllers.allObjects
- .map { controller in
- RefetchChannelListQueryOperation(
- controller: controller,
- context: context
- )
- }
- operations.append(contentsOf: refetchChannelListQueryOperations)
-
- let channelListQueries: [ChannelListQuery] = {
- let queries = activeChannelLists.allObjects
- .map(\.query)
- .map { ($0.filter.filterHash, $0) }
- let uniqueQueries = Dictionary(queries, uniquingKeysWith: { _, last in last })
- return Array(uniqueQueries.values)
- }()
- operations.append(contentsOf: channelListQueries
- .map { channelListQuery in
- RefetchChannelListQueryOperation(
- query: channelListQuery,
- channelListUpdater: channelListUpdater,
- context: context
- )
- })
-
- // 4. Clean up unwanted channels
- operations.append(DeleteUnwantedChannelsOperation(database: database, context: context))
-
- operations.append(BlockOperation(block: { [weak self] in
- log.info("Finished recovering offline state", subsystems: .offlineSupport)
- DispatchQueue.main.async {
- self?.apiClient.exitRecoveryMode()
- completion()
- }
- }))
-
- // We are making sure the operations happen sequentially one after the other by setting one as the dependency
- // of the following one
- var previousOperation: Operation?
- operations.reversed().forEach { operation in
- defer { previousOperation = operation }
- guard let previousOperation = previousOperation else { return }
- previousOperation.addDependency(operation)
- }
-
- operationQueue.addOperations(operations, waitUntilFinished: false)
- }
/// Syncs the events for the active chat channels using the last sync date.
/// - Parameter completion: A block that will get executed upon completion of the synchronization
diff --git a/Sources/StreamChat/Workers/ChannelListUpdater.swift b/Sources/StreamChat/Workers/ChannelListUpdater.swift
index 8fcd4156d1a..5f8de35b898 100644
--- a/Sources/StreamChat/Workers/ChannelListUpdater.swift
+++ b/Sources/StreamChat/Workers/ChannelListUpdater.swift
@@ -42,8 +42,14 @@ class ChannelListUpdater: Worker {
}
func refreshLoadedChannels(for query: ChannelListQuery, channelCount: Int, completion: @escaping (Result, Error>) -> Void) {
+ guard channelCount > 0 else {
+ completion(.success(Set()))
+ return
+ }
+
var allPages = [ChannelListQuery]()
- for offset in stride(from: 0, to: channelCount, by: .channelsPageSize) {
+ let pageSize = query.pagination.pageSize > 0 ? query.pagination.pageSize : .channelsPageSize
+ for offset in stride(from: 0, to: channelCount, by: pageSize) {
var pageQuery = query
pageQuery.pagination = Pagination(pageSize: .channelsPageSize, offset: offset)
allPages.append(pageQuery)
@@ -90,66 +96,6 @@ class ChannelListUpdater: Worker {
}
}
}
-
- func resetChannelsQuery(
- for query: ChannelListQuery,
- pageSize: Int,
- watchedAndSynchedChannelIds: Set,
- synchedChannelIds: Set,
- completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>) -> Void
- ) {
- var updatedQuery = query
- updatedQuery.pagination = .init(pageSize: pageSize, offset: 0)
-
- var unwantedCids = Set()
- // Fetches the channels matching the query, and stores them in the database.
- apiClient.recoveryRequest(endpoint: .channels(query: query)) { [weak self] result in
- switch result {
- case let .success(channelListPayload):
- self?.writeChannelListPayload(
- payload: channelListPayload,
- query: updatedQuery,
- initialActions: { session in
- guard let queryDTO = session.channelListQuery(filterHash: updatedQuery.filter.filterHash) else { return }
-
- let localQueryCIDs = Set(queryDTO.channels.compactMap { try? ChannelId(cid: $0.cid) })
- let remoteQueryCIDs = Set(channelListPayload.channels.map(\.channel.cid))
-
- let updatedChannels = synchedChannelIds.union(watchedAndSynchedChannelIds)
- let localNotInRemote = localQueryCIDs.subtracting(remoteQueryCIDs)
- let localInRemote = localQueryCIDs.intersection(remoteQueryCIDs)
-
- // We unlink those local channels that are no longer in remote
- for cid in localNotInRemote {
- guard let channelDTO = session.channel(cid: cid) else { continue }
- queryDTO.channels.remove(channelDTO)
- }
-
- // We are going to clean those channels that are present in the both the local and remote query,
- // and that have not been synched nor watched. Those are outdated, can contain gaps.
- let cidsToClean = localInRemote.subtracting(updatedChannels)
- session.cleanChannels(cids: cidsToClean)
-
- // We are also going to keep track of the unwanted channels
- // Those are the ones that exist locally but we are not interested in anymore in this context.
- // In this case, it is going to query local ones not appearing in remote, subtracting the ones
- // that are already being watched.
- unwantedCids = localNotInRemote.subtracting(watchedAndSynchedChannelIds)
- },
- completion: { result in
- switch result {
- case let .success(newSynchedAndWatchedChannels):
- completion(.success((newSynchedAndWatchedChannels, unwantedCids)))
- case let .failure(error):
- completion(.failure(error))
- }
- }
- )
- case let .failure(error):
- completion(.failure(error))
- }
- }
- }
/// Starts watching the channels with the given ids and updates the channels in the local storage.
///
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift
index b35e0a81822..22a2ae8e440 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift
@@ -9,7 +9,6 @@ class ChatChannelListController_Mock: ChatChannelListController, Spy {
let spyState = SpyState()
var loadNextChannelsIsCalled = false
var loadNextChannelsCallCount = 0
- var resetChannelsQueryResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>?
var refreshLoadedChannelsResult: Result, any Error>?
/// Creates a new mock instance of `ChatChannelListController`.
@@ -37,15 +36,6 @@ class ChatChannelListController_Mock: ChatChannelListController, Spy {
record()
refreshLoadedChannelsResult.map(completion)
}
-
- override func resetQuery(
- watchedAndSynchedChannelIds: Set,
- synchedChannelIds: Set,
- completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>) -> Void
- ) {
- record()
- resetChannelsQueryResult.map(completion)
- }
}
extension ChatChannelListController_Mock {
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
index e1e9686379b..c82d7439b1d 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
@@ -98,10 +98,6 @@ class DatabaseSession_Mock: DatabaseSession {
underlyingSession.deleteQuery(query)
}
- func cleanChannels(cids: Set) {
- underlyingSession.cleanChannels(cids: cids)
- }
-
func removeChannels(cids: Set) {
underlyingSession.removeChannels(cids: cids)
}
diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift
index b2784a97293..fe9aa5510ba 100644
--- a/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift
+++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift
@@ -16,7 +16,7 @@ final class ChannelListUpdater_Spy: ChannelListUpdater, Spy {
@Atomic var fetch_queries: [ChannelListQuery] = []
@Atomic var fetch_completion: ((Result) -> Void)?
- var resetChannelsQueryResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>?
+ @Atomic var refreshLoadedChannelsResult: Result, Error>?
@Atomic var markAllRead_completion: ((Error?) -> Void)?
@@ -63,16 +63,14 @@ final class ChannelListUpdater_Spy: ChannelListUpdater, Spy {
_fetch_queries.mutate { $0.append(channelListQuery) }
fetch_completion = completion
}
-
- override func resetChannelsQuery(
+
+ override func refreshLoadedChannels(
for query: ChannelListQuery,
- pageSize: Int,
- watchedAndSynchedChannelIds: Set,
- synchedChannelIds: Set,
- completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>) -> Void
+ channelCount: Int,
+ completion: @escaping (Result, any Error>) -> Void
) {
record()
- resetChannelsQueryResult.map(completion)
+ refreshLoadedChannelsResult?.invoke(with: completion)
}
override func link(
diff --git a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift
index add00a7a6a8..1b135976289 100644
--- a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift
+++ b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift
@@ -818,11 +818,9 @@ final class ChannelListController_Tests: XCTestCase {
AssertAsync.canBeReleased(&weakController)
}
- // MARK: - Reset query
-
- func test_resetQuery_whenSucceeds_updates_hasLoadedAllPreviousChannels_whenRecevingAFullPage() {
- XCTAssertFalse(controller.hasLoadedAllPreviousChannels)
+ // MARK: - Refresh Loaded Channels
+ func test_refreshLoadedChannels_whenSucceedsThenControllerSucceeds() {
// Simulate synchronize to create all dependencies
controller.synchronize()
@@ -830,51 +828,20 @@ final class ChannelListController_Tests: XCTestCase {
let channels: [ChatChannel] = (0.. [ChannelDTO] {
try database.viewContext.fetch(ChannelDTO.allChannelsFetchRequest)
}
diff --git a/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift
index 76df92b83d5..030e1a25791 100644
--- a/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift
+++ b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift
@@ -6,34 +6,6 @@
@testable import StreamChatTestTools
import XCTest
-class SyncRepositoryV2_Tests: SyncRepository_Tests {
- override func setUp() {
- super.setUp()
- repository.usesV2Sync = true
- }
-
- func test_syncLocalEvents_bySkippingAlreadyFetchedChannelIds() throws {
- let lastSyncDate = Date()
- let cid = ChannelId.unique
- try prepareForSyncLocalStorage(
- createUser: true,
- lastSynchedEventDate: lastSyncDate,
- createChannel: true,
- cid: cid
- )
-
- // One channel list controller which fetches the state for cid
- let chatListController = ChatChannelListController_Mock(query: .init(filter: .exists(.cid)), client: client)
- chatListController.state_mock = .remoteDataFetched
- chatListController.channels_mock = [.mock(cid: cid)]
- repository.startTrackingChannelListController(chatListController)
- chatListController.refreshLoadedChannelsResult = .success(Set([cid]))
-
- // If it fails, it means /sync was called but we expect it to be skipped because channel list refresh already refreshed the channel
- waitForSyncLocalStateRun()
- }
-}
-
class SyncRepository_Tests: XCTestCase {
var client: ChatClient_Mock!
var offlineRequestsRepository: OfflineRequestsRepository_Mock!
@@ -71,7 +43,6 @@ class SyncRepository_Tests: XCTestCase {
apiClient: apiClient,
channelListUpdater: channelListUpdater
)
- repository.usesV2Sync = false
}
override func tearDown() {
@@ -187,33 +158,6 @@ class SyncRepository_Tests: XCTestCase {
XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1)
}
- func test_syncLocalState_localStorageEnabled_pendingConnectionDate_channels() throws {
- try XCTSkipIf(repository.usesV2Sync, "V2 only syncs if there are active controllers")
-
- let channelId = ChannelId.unique
- try prepareForSyncLocalStorage(
- createUser: true,
- lastSynchedEventDate: Date().addingTimeInterval(-3600),
- createChannel: true,
- cid: channelId
- )
-
- let firstEventDate = Date.unique
- let secondEventDate = Date.unique
- let payload = messageEventPayload(cid: channelId, with: [firstEventDate, secondEventDate])
- waitForSyncLocalStateRun(requestResult: .success(payload))
-
- // Should use first event's created at date
- XCTAssertEqual(lastSyncAtValue, secondEventDate)
- // Write: API Response, lastSyncAt
- XCTAssertEqual(database.writeSessionCounter, 2)
- XCTAssertEqual(repository.activeChannelControllers.count, 0)
- XCTAssertEqual(repository.activeChannelListControllers.count, 0)
- XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 1)
- XCTAssertEqual(apiClient.request_allRecordedCalls.count, 0)
- XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1)
- }
-
func test_syncLocalState_localStorageEnabled_pendingConnectionDate_channels_activeRemoteChannelController() throws {
let cid = ChannelId.unique
try prepareForSyncLocalStorage(
@@ -242,19 +186,11 @@ class SyncRepository_Tests: XCTestCase {
// Write: API Response, lastSyncAt
XCTAssertEqual(database.writeSessionCounter, 2)
XCTAssertEqual(repository.activeChannelControllers.count, 1)
- if repository.usesV2Sync {
- XCTAssertCall("watch()", on: chat, times: 1)
- } else {
- XCTAssertCall("recoverWatchedChannel(recovery:completion:)", on: chatController, times: 1)
- }
+ XCTAssertCall("watch()", on: chat, times: 1)
+
XCTAssertEqual(repository.activeChannelListControllers.count, 0)
- if repository.usesV2Sync {
- XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 0)
- XCTAssertEqual(apiClient.request_allRecordedCalls.count, 1)
- } else {
- XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 1)
- XCTAssertEqual(apiClient.request_allRecordedCalls.count, 0)
- }
+ XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 0)
+ XCTAssertEqual(apiClient.request_allRecordedCalls.count, 1)
XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1)
}
@@ -271,11 +207,7 @@ class SyncRepository_Tests: XCTestCase {
chatListController.state_mock = .remoteDataFetched
chatListController.channels_mock = [.mock(cid: cid)]
repository.startTrackingChannelListController(chatListController)
- if repository.usesV2Sync {
- chatListController.refreshLoadedChannelsResult = .success(Set())
- } else {
- chatListController.resetChannelsQueryResult = .success(([], []))
- }
+ chatListController.refreshLoadedChannelsResult = .success(Set())
let eventDate = Date.unique
waitForSyncLocalStateRun(requestResult: .success(messageEventPayload(cid: cid, with: [eventDate])))
@@ -286,57 +218,12 @@ class SyncRepository_Tests: XCTestCase {
XCTAssertEqual(database.writeSessionCounter, 2)
XCTAssertEqual(repository.activeChannelControllers.count, 0)
XCTAssertEqual(repository.activeChannelListControllers.count, 1)
- if repository.usesV2Sync {
- XCTAssertCall(
- "refreshLoadedChannels(completion:)", on: chatListController,
- times: 1
- )
- XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 0)
- XCTAssertEqual(apiClient.request_allRecordedCalls.count, 1)
- } else {
- XCTAssertCall(
- "resetQuery(watchedAndSynchedChannelIds:synchedChannelIds:completion:)", on: chatListController,
- times: 1
- )
- XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 1)
- XCTAssertEqual(apiClient.request_allRecordedCalls.count, 0)
- }
- XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1)
- }
-
- func test_syncLocalState_localStorageEnabled_pendingConnectionDate_channels_activeRemoteChannelListController_unwantedChannels(
- ) throws {
- try XCTSkipIf(repository.usesV2Sync, "V2 does not handle unwanted channels")
-
- let cid = ChannelId.unique
- try prepareForSyncLocalStorage(
- createUser: true,
- lastSynchedEventDate: Date().addingTimeInterval(-3600),
- createChannel: true,
- cid: cid
- )
-
- let chatListController = ChatChannelListController_Mock(query: .init(filter: .exists(.cid)), client: client)
- chatListController.state_mock = .remoteDataFetched
- repository.startTrackingChannelListController(chatListController)
- let unwantedId = ChannelId.unique
- chatListController.resetChannelsQueryResult = .success(([], [unwantedId]))
-
- let eventDate = Date.unique
- waitForSyncLocalStateRun(requestResult: .success(messageEventPayload(cid: cid, with: [eventDate])))
-
- // Should use first event's created at date
- XCTAssertEqual(lastSyncAtValue, eventDate)
- // Write: API Response, unwanted channels, lastSyncAt
- XCTAssertEqual(database.writeSessionCounter, 3)
- XCTAssertEqual(repository.activeChannelControllers.count, 0)
- XCTAssertEqual(repository.activeChannelListControllers.count, 1)
XCTAssertCall(
- "resetQuery(watchedAndSynchedChannelIds:synchedChannelIds:completion:)", on: chatListController,
+ "refreshLoadedChannels(completion:)", on: chatListController,
times: 1
)
- XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 1)
- XCTAssertEqual(apiClient.request_allRecordedCalls.count, 0)
+ XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 0)
+ XCTAssertEqual(apiClient.request_allRecordedCalls.count, 1)
XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1)
}
@@ -351,9 +238,7 @@ class SyncRepository_Tests: XCTestCase {
)
let channelController = ChatChannelController_Mock(channelQuery: ChannelQuery(cid: .unique), channelListQuery: nil, client: client)
- if repository.usesV2Sync {
- repository.startTrackingChannelController(channelController)
- }
+ repository.startTrackingChannelController(channelController)
let firstDate = lastSyncDate.addingTimeInterval(1)
let secondDate = lastSyncDate.addingTimeInterval(2)
@@ -370,6 +255,27 @@ class SyncRepository_Tests: XCTestCase {
repository.stopTrackingChannelController(channelController)
}
+
+ func test_syncLocalEvents_bySkippingAlreadyFetchedChannelIds() throws {
+ let lastSyncDate = Date()
+ let cid = ChannelId.unique
+ try prepareForSyncLocalStorage(
+ createUser: true,
+ lastSynchedEventDate: lastSyncDate,
+ createChannel: true,
+ cid: cid
+ )
+
+ // One channel list controller which fetches the state for cid
+ let chatListController = ChatChannelListController_Mock(query: .init(filter: .exists(.cid)), client: client)
+ chatListController.state_mock = .remoteDataFetched
+ chatListController.channels_mock = [.mock(cid: cid)]
+ repository.startTrackingChannelListController(chatListController)
+ chatListController.refreshLoadedChannelsResult = .success(Set([cid]))
+
+ // If it fails, it means /sync was called but we expect it to be skipped because channel list refresh already refreshed the channel
+ waitForSyncLocalStateRun()
+ }
// MARK: - Sync existing channels events
@@ -695,63 +601,6 @@ class SyncRepository_Tests: XCTestCase {
// THEN
waitForExpectations(timeout: defaultTimeout, handler: nil)
}
-
- func test_cancelRecoveryFlow_cancelsAllOperations() throws {
- try XCTSkipIf(repository.usesV2Sync, "V2 has different implementation")
- // Prepare environment
- try prepareForSyncLocalStorage(
- createUser: true,
- lastSynchedEventDate: Date(),
- createChannel: true
- )
-
- // Add active channel component
- let channelQuery = ChannelQuery(cid: .unique)
- let channelController = ChatChannelController(channelQuery: channelQuery, channelListQuery: nil, client: client)
- channelController.state = .remoteDataFetched
- repository.startTrackingChannelController(channelController)
-
- // Add active channel list component
- let channelListController = ChatChannelListController_Mock(query: .init(filter: .exists(.cid)), client: client)
- channelListController.state_mock = .remoteDataFetched
- repository.startTrackingChannelListController(channelListController)
-
- // Sync local state
- var completionCalled = false
- repository.syncLocalState {
- completionCalled = true
- }
-
- // Wait for /sync to be called
- if repository.usesV2Sync {
- apiClient.waitForRequest()
- } else {
- apiClient.waitForRecoveryRequest()
- }
-
- // Let /sync operation to complete
- let syncResponse = Result.success(.init(eventPayloads: []))
- apiClient.test_simulateRecoveryResponse(syncResponse)
- apiClient.recoveryRequest_completion = nil
-
- // Wait for watch operation
- if !repository.usesV2Sync {
- AssertAsync.willBeTrue(apiClient.recoveryRequest_completion != nil)
- }
-
- // Cancel recovery flow
- repository.cancelRecoveryFlow()
-
- // Let watch operation to complete
- let watchResponse = Result.success(dummyPayload(with: channelQuery.cid!))
- apiClient.test_simulateRecoveryResponse(watchResponse)
-
- // Assert left operations are not executed
- AssertAsync {
- Assert.staysTrue(channelListController.recordedFunctions.isEmpty)
- Assert.staysFalse(completionCalled)
- }
- }
// MARK: - Tracking
@@ -852,28 +701,13 @@ extension SyncRepository_Tests {
expectation.fulfill()
}
- if !repository.usesV2Sync {
- AssertAsync.willBeTrue(
- "enterRecoveryMode()".wasCalled(on: apiClient, times: 1)
- )
- }
-
if let result = requestResult {
- if repository.usesV2Sync {
- apiClient.waitForRequest()
- guard let callback = apiClient.request_completion as? (Result) -> Void else {
- XCTFail("A request for /sync should have been executed")
- return
- }
- callback(result)
- } else {
- apiClient.waitForRecoveryRequest()
- guard let callback = apiClient.recoveryRequest_completion as? (Result) -> Void else {
- XCTFail("A request for /sync should have been executed")
- return
- }
- callback(result)
+ apiClient.waitForRequest()
+ guard let callback = apiClient.request_completion as? (Result) -> Void else {
+ XCTFail("A request for /sync should have been executed")
+ return
}
+ callback(result)
}
waitForExpectations(timeout: defaultTimeout, handler: nil)
diff --git a/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift b/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift
index 8a61c77cb09..aa8d19f08ff 100644
--- a/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift
+++ b/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift
@@ -226,170 +226,6 @@ final class ChannelListUpdater_Tests: XCTestCase {
XCTAssertEqual(channelsFromQuery.count, 3)
}
- // MARK: - Reset Channels Query
-
- func test_resetChannelsQueryGreenPath() throws {
- var query = ChannelListQuery(filter: .in(.members, values: [.unique]))
- query.pagination = Pagination(pageSize: 10, offset: 4)
-
- try database.writeSynchronously { session in
- session.saveQuery(query: query)
- }
-
- let expectation = self.expectation(description: "resetChannelsQuery completion")
- var receivedResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>!
- listUpdater.resetChannelsQuery(
- for: query,
- pageSize: query.pagination.pageSize,
- watchedAndSynchedChannelIds: Set(),
- synchedChannelIds: Set()
- ) { result in
- receivedResult = result
- expectation.fulfill()
- }
-
- // Simulate API response with channel data
- let cid = ChannelId(type: .messaging, id: .unique)
- let payload = ChannelListPayload(channels: [dummyPayload(with: cid)])
- apiClient.test_simulateRecoveryResponse(.success(payload))
-
- waitForExpectations(timeout: defaultTimeout, handler: nil)
-
- let requests = apiClient.recoveryRequest_allRecordedCalls
- XCTAssertEqual(requests.count, 1)
- XCTAssertFalse(receivedResult.isError)
-
- // Should reset pagination
- query.pagination = Pagination(pageSize: 20, offset: 0)
- let expectedBody = ["payload": query]
- XCTAssertEqual(requests.first?.0.body, expectedBody.asAnyEncodable)
- }
-
- func test_resetChannelsQuery_QueryNotInDatabase() throws {
- let userId = "UserId"
- var query = ChannelListQuery(filter: .in(.members, values: [userId]))
- try database.writeSynchronously { session in
- try session.saveUser(payload: .dummy(userId: userId))
- }
-
- let expectation = self.expectation(description: "resetChannelsQuery completion")
- var receivedResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>!
- listUpdater.resetChannelsQuery(
- for: query,
- pageSize: query.pagination.pageSize,
- watchedAndSynchedChannelIds: Set(),
- synchedChannelIds: Set()
- ) { result in
- receivedResult = result
- expectation.fulfill()
- }
-
- // Simulate API response with channel data
- let cid = ChannelId(type: .messaging, id: "newChannel")
- let payload = ChannelListPayload(
- channels: [dummyPayload(with: cid, members: [.dummy(user: .dummy(userId: userId))])]
- )
- apiClient.test_simulateRecoveryResponse(.success(payload))
-
- waitForExpectations(timeout: defaultTimeout, handler: nil)
-
- let requests = apiClient.recoveryRequest_allRecordedCalls
- XCTAssertEqual(requests.count, 1)
- XCTAssertNil(receivedResult.error)
-
- // If the query does not exist, the payload should still be saved in database
- XCTAssertEqual(channels(for: query, database: database).count, 1)
-
- // Should reset pagination
- query.pagination = Pagination(pageSize: 20, offset: 0)
- let expectedBody = ["payload": query]
- XCTAssertEqual(requests.first?.0.body, expectedBody.asAnyEncodable)
- }
-
- func test_resetChannelsQuery_shouldOnlyRemoveOutdatedAndNotWatchedChannels() throws {
- // Preparation of the environment
- let userId = "UserId"
- let query = ChannelListQuery(filter: .in(.members, values: [userId]))
-
- let syncedId1 = ChannelId(type: .messaging, id: "syncedId1")
- let syncedId2 = ChannelId(type: .messaging, id: "syncedId2")
- let localId = ChannelId(type: .messaging, id: "localId")
- let outdatedId = ChannelId(type: .messaging, id: "outdatedId")
- let watchedAndSynchedId = ChannelId(type: .messaging, id: "watchedAndSynchedId")
- let syncedAndWatchedId = ChannelId(type: .messaging, id: "syncedAndWatchedId")
- let newRemoteChannel = ChannelId(type: .messaging, id: "newRemoteChannel")
- let watchedChannelIds = Set([syncedAndWatchedId, watchedAndSynchedId])
- let synchedChannelIds = Set([syncedId1, syncedId2, syncedAndWatchedId])
-
- try database.writeSynchronously { session in
- try session.saveUser(payload: .dummy(userId: userId))
- try [syncedId1, syncedId2, outdatedId, watchedAndSynchedId, syncedAndWatchedId, localId].forEach {
- let payload = self.dummyPayload(with: $0, members: [.dummy(user: .dummy(userId: userId))])
- try session.saveChannel(payload: payload, query: query, cache: nil)
- }
- }
-
- XCTAssertEqual(channels(for: query, database: database).count, 6)
-
- // Reset Channels Query
- let expectation = self.expectation(description: "resetChannelsQuery completion")
- var receivedResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>!
- listUpdater.resetChannelsQuery(
- for: query,
- pageSize: query.pagination.pageSize,
- watchedAndSynchedChannelIds: watchedChannelIds,
- synchedChannelIds: synchedChannelIds
- ) { result in
- receivedResult = result
- expectation.fulfill()
- }
-
- // Simulate API response with channel data
- let payload = ChannelListPayload(channels: [syncedAndWatchedId, syncedId2, newRemoteChannel, localId].map {
- self.dummyPayload(with: $0, numberOfMessages: 0, members: [.dummy(user: .dummy(userId: userId))])
- })
- apiClient.test_simulateRecoveryResponse(.success(payload))
-
- waitForExpectations(timeout: defaultTimeout, handler: nil)
-
- // EXPECTED RESULTS:
- // syncedId1 -> Not present in remote query, but synched: Unwanted -
- // syncedId2 -> Present in local and remote query, synched: Kept 1
- // outdatedId -> Not present in remote query, not synched: Unwanted -
- // localId -> Present in local and remote query, not synched: Cleaned 2
- // watchedAndSynchedId -> Not present in remote query, but watched: Unlinked -
- // syncedAndWatchedId -> Present in local and remote query, watched: Kept 3
- // newRemoteChannel -> Present in remote query only: Added 4
-
- let requests = apiClient.recoveryRequest_allRecordedCalls
- XCTAssertEqual(requests.count, 1)
- XCTAssertFalse(receivedResult.isError)
-
- // Two channels were marked as unwanted
- XCTAssertEqual(receivedResult.value?.unwanted.count, 2)
- XCTAssertTrue(receivedResult.value?.unwanted.contains { $0 == outdatedId } == true)
- XCTAssertTrue(receivedResult.value?.unwanted.contains { $0 == syncedId1 } == true)
-
- // Four channels were synched and watched, and are now part of the query
- XCTAssertEqual(receivedResult.value?.synchedAndWatched.count, 4)
- let queryChannels = channels(for: query, database: database)
- XCTAssertEqual(queryChannels.count, 4)
- [syncedId2, localId, syncedAndWatchedId, newRemoteChannel].forEach { cid in
- XCTAssertTrue(queryChannels.contains { $0.cid == cid.rawValue })
- }
-
- // No channel should have been removed yet here
- let allChannels = (try? database.viewContext.fetch(ChannelDTO.allChannelsFetchRequest)) ?? []
- XCTAssertEqual(allChannels.count, 7)
-
- // Cleaned channel should not have messages
- XCTAssertEqual(queryChannels.first { $0.cid == localId.rawValue }?.messages.count, 0)
-
- // Unlinked channels should not have been cleared
- XCTAssertEqual(allChannels.first { $0.cid == syncedId1.rawValue }?.messages.count, 1)
- XCTAssertEqual(allChannels.first { $0.cid == watchedAndSynchedId.rawValue }?.messages.count, 1)
- }
-
// MARK: - Fetch
func test_fetch_makesCorrectAPICall() {
@@ -441,6 +277,39 @@ final class ChannelListUpdater_Tests: XCTestCase {
// Assert updater can be deallocated without waiting for the API response.
AssertAsync.canBeReleased(&listUpdater)
}
+
+ // MARK: - Refresh Loaded Channels
+
+ func test_refreshLoadedChannels_whenEmpty_thenRefreshDoesNotHappen() async throws {
+ var query = ChannelListQuery(filter: .in(.members, values: [.unique]))
+ query.pagination = Pagination(pageSize: 10)
+ let initialLoadedChannelCount = 0
+
+ let cids = try await listUpdater.refreshLoadedChannels(for: query, channelCount: initialLoadedChannelCount)
+ XCTAssertEqual(Set(), cids)
+ }
+
+ func test_refreshLoadedChannels_whenMultiplePagesAreLoaded_thenAllPagesAreReloaded() async throws {
+ let pageSize = Int.channelsPageSize
+ var query = ChannelListQuery(filter: .in(.members, values: [.unique]))
+ query.pagination = Pagination(pageSize: pageSize)
+
+ let initialChannels = (0..