From 3fae8248f4a425583bb24cd152d373c9dd218ffa Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 19 Dec 2024 21:48:18 -0800 Subject: [PATCH 1/4] performance follow ups --- Package.swift | 2 +- Sources/XMTPiOS/Client.swift | 76 ++++++++++--------- XMTP.podspec | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/Package.swift b/Package.swift index 46fe7a30..23d9b5ee 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( .package(url: "https://github.com/bufbuild/connect-swift", exact: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", exact: "1.8.3"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "3.0.13") + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "3.0.14") ], targets: [ .target( diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 70d6736f..b7b54728 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -84,6 +84,18 @@ public struct ClientOptions { } } +actor ApiClientCache { + private var apiClientCache: [String: XmtpApiClient] = [:] + + func getClient(forKey key: String) -> XmtpApiClient? { + return apiClientCache[key] + } + + func setClient(_ client: XmtpApiClient, forKey key: String) { + apiClientCache[key] = client + } +} + public final class Client { public let address: String public let inboxID: String @@ -91,8 +103,8 @@ public final class Client { public let dbPath: String public let installationID: String public let environment: XMTPEnvironment - public let apiClient: XmtpApiClient private let ffiClient: LibXMTP.FfiXmtpClient + private static let apiCache = ApiClientCache() public lazy var conversations: Conversations = .init( client: self, ffiConversations: ffiClient.conversations()) @@ -112,12 +124,11 @@ public final class Client { inboxId: String, apiClient: XmtpApiClient? = nil ) async throws -> Client { - let (libxmtpClient, dbPath, apiClient) = try await initFFiClient( + let (libxmtpClient, dbPath) = try await initFFiClient( accountAddress: accountAddress.lowercased(), options: options, signingKey: signingKey, - inboxId: inboxId, - apiClient: apiClient + inboxId: inboxId ) let client = try Client( @@ -126,8 +137,7 @@ public final class Client { dbPath: dbPath, installationID: libxmtpClient.installationId().toHex, inboxID: libxmtpClient.inboxId(), - environment: options.api.env, - apiClient: apiClient + environment: options.api.env ) // Register codecs @@ -139,8 +149,7 @@ public final class Client { } public static func create( - account: SigningKey, options: ClientOptions, - apiClient: XmtpApiClient? = nil + account: SigningKey, options: ClientOptions ) async throws -> Client { @@ -152,14 +161,12 @@ public final class Client { accountAddress: accountAddress, options: options, signingKey: account, - inboxId: inboxId, - apiClient: apiClient + inboxId: inboxId ) } public static func build( - address: String, options: ClientOptions, inboxId: String? = nil, - apiClient: XmtpApiClient? = nil + address: String, options: ClientOptions, inboxId: String? = nil ) async throws -> Client { @@ -176,8 +183,7 @@ public final class Client { accountAddress: accountAddress, options: options, signingKey: nil, - inboxId: resolvedInboxId, - apiClient: apiClient + inboxId: resolvedInboxId ) } @@ -185,9 +191,8 @@ public final class Client { accountAddress: String, options: ClientOptions, signingKey: SigningKey?, - inboxId: String, - apiClient: XmtpApiClient? = nil - ) async throws -> (FfiXmtpClient, String, XmtpApiClient) { + inboxId: String + ) async throws -> (FfiXmtpClient, String) { let address = accountAddress.lowercased() let mlsDbDirectory = options.dbDirectory @@ -214,15 +219,8 @@ public final class Client { let alias = "xmtp-\(options.api.env.rawValue)-\(inboxId).db3" let dbURL = directoryURL.appendingPathComponent(alias).path - let xmtpApiClient: XmtpApiClient - if let existingApiClient = apiClient { - xmtpApiClient = existingApiClient - } else { - xmtpApiClient = try await connectToApiBackend(api: options.api) - } - let ffiClient = try await LibXMTP.createClient( - api: xmtpApiClient, + api: connectToApiBackend(api: options.api), db: dbURL, encryptionKey: options.dbEncryptionKey, inboxId: inboxId, @@ -252,7 +250,7 @@ public final class Client { } } - return (ffiClient, dbURL, xmtpApiClient) + return (ffiClient, dbURL) } private static func handleSignature( @@ -282,12 +280,19 @@ public final class Client { } } - public static func connectToApiBackend( - api: ClientOptions.Api - ) async throws -> XmtpApiClient { - return try await connectToBackend( - host: api.env.url, - isSecure: api.env.isSecure == true) + public static func connectToApiBackend(api: ClientOptions.Api) async throws + -> XmtpApiClient + { + let cacheKey = api.env.url + + if let cachedClient = await apiCache.getClient(forKey: cacheKey) { + return cachedClient + } + + let apiClient = try await connectToBackend( + host: api.env.url, isSecure: api.isSecure) + await apiCache.setClient(apiClient, forKey: cacheKey) + return apiClient } public static func getOrCreateInboxId( @@ -297,8 +302,7 @@ public final class Client { do { inboxId = try await getInboxIdForAddress( - host: api.env.url, - isSecure: api.env.isSecure == true, + api: connectToApiBackend(api: api), accountAddress: address.lowercased() ) ?? generateInboxId( @@ -344,8 +348,7 @@ public final class Client { init( address: String, ffiClient: LibXMTP.FfiXmtpClient, dbPath: String, - installationID: String, inboxID: String, environment: XMTPEnvironment, - apiClient: XmtpApiClient + installationID: String, inboxID: String, environment: XMTPEnvironment ) throws { self.address = address self.ffiClient = ffiClient @@ -353,7 +356,6 @@ public final class Client { self.installationID = installationID self.inboxID = inboxID self.environment = environment - self.apiClient = apiClient } public func addAccount(newAccount: SigningKey) diff --git a/XMTP.podspec b/XMTP.podspec index eb213ddf..621f8976 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "XMTP" - spec.version = "3.0.19" + spec.version = "3.0.20" spec.summary = "XMTP SDK Cocoapod" @@ -23,7 +23,7 @@ Pod::Spec.new do |spec| spec.dependency 'CSecp256k1', '~> 0.2' spec.dependency "Connect-Swift", "= 1.0.0" - spec.dependency 'LibXMTP', '= 3.0.13' + spec.dependency 'LibXMTP', '= 3.0.14' spec.dependency 'CryptoSwift', '= 1.8.3' spec.dependency 'SQLCipher', '= 4.5.7' diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7b70aa78..1bca2503 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "203fd6d67bb72e3114b273ce9bbddd6fc747d583", - "version" : "3.0.13" + "revision" : "9cf94acdea7d03008b1f130a9848c38ee6b711de", + "version" : "3.0.14" } }, { From 0a399b0aa01bd8c8fe201f1c75c8ebde29be3ebc Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 19 Dec 2024 21:56:59 -0800 Subject: [PATCH 2/4] write a test --- Tests/XMTPTests/ClientTests.swift | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index 8127933c..2266571b 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -532,19 +532,18 @@ class ClientTests: XCTestCase { print("PERF: Built a client with inboxId in \(time3)s") // Measure time to build a client with an inboxId and apiClient + try await Client.connectToApiBackend(api: ClientOptions.Api(env: .dev, isSecure: true)) let start4 = Date() - let buildClient3 = try await Client.build( - address: fakeWallet.address, + try await Client.create( + account: fakeWallet, options: ClientOptions( api: ClientOptions.Api(env: .dev, isSecure: true), dbEncryptionKey: key - ), - inboxId: client.inboxID, - apiClient: client.apiClient + ) ) let end4 = Date() let time4 = end4.timeIntervalSince(start4) - print("PERF: Built a client with inboxId and apiClient in \(time4)s") + print("PERF: Create a client with prebuild in \(time4)s") // Assert performance comparisons XCTAssertTrue( @@ -560,15 +559,7 @@ class ClientTests: XCTestCase { ) XCTAssertTrue( time4 < time1, - "Building a client with apiClient should be faster than creating one." - ) - XCTAssertTrue( - time4 < time2, - "Building a client with apiClient should be faster than building one." - ) - XCTAssertTrue( - time4 < time2, - "Building a client with apiClient should be faster than building one with inboxId." + "Creating a client with apiClient should be faster than creating one without." ) // Assert that inbox IDs match @@ -580,10 +571,6 @@ class ClientTests: XCTestCase { client.inboxID, buildClient2.inboxID, "Inbox ID of the created client and second built client should match." ) - XCTAssertEqual( - client.inboxID, buildClient3.inboxID, - "Inbox ID of the created client and second built client should match." - ) } } From 6437b4f55f3be16b5f8ffdaac729fcb3d1f3cc1d Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 19 Dec 2024 22:12:47 -0800 Subject: [PATCH 3/4] re-enable history syncing and add more tests --- Sources/XMTPiOS/Client.swift | 6 +- Sources/XMTPiOS/PrivatePreferences.swift | 4 - Tests/XMTPTests/ClientTests.swift | 28 ++-- Tests/XMTPTests/ConversationTests.swift | 119 ---------------- Tests/XMTPTests/HistorySyncTests.swift | 170 +++++++++++++++++++++++ 5 files changed, 187 insertions(+), 140 deletions(-) create mode 100644 Tests/XMTPTests/HistorySyncTests.swift diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index b7b54728..c9d47ad0 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -227,7 +227,7 @@ public final class Client { accountAddress: address, nonce: 0, legacySignedPrivateKeyProto: nil, - historySyncUrl: nil + historySyncUrl: options.historySyncUrl ) try await options.preAuthenticateToInboxCallback?() @@ -540,10 +540,6 @@ public final class Client { } } - public func requestMessageHistorySync() async throws { - try await ffiClient.sendSyncRequest(kind: .messages) - } - public func inboxState(refreshFromNetwork: Bool) async throws -> InboxState { return InboxState( diff --git a/Sources/XMTPiOS/PrivatePreferences.swift b/Sources/XMTPiOS/PrivatePreferences.swift index 9c6d9c03..de6b8e43 100644 --- a/Sources/XMTPiOS/PrivatePreferences.swift +++ b/Sources/XMTPiOS/PrivatePreferences.swift @@ -83,10 +83,6 @@ public actor PrivatePreferences { ).fromFFI } - public func syncConsent() async throws { - try await ffiClient.sendSyncRequest(kind: .consent) - } - public func streamConsent() -> AsyncThrowingStream { diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index 2266571b..14bda0a4 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -304,28 +304,31 @@ class ClientTests: XCTestCase { func testRevokesAllOtherInstallations() async throws { let key = try Crypto.secureRandomBytes(count: 32) let alix = try PrivateKey.generate() - let options = ClientOptions.init( - api: .init(env: .local, isSecure: false), - dbEncryptionKey: key - ) let alixClient = try await Client.create( account: alix, - options: options + options: ClientOptions.init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key + ) ) - try alixClient.dropLocalDatabaseConnection() - try alixClient.deleteLocalDatabase() let alixClient2 = try await Client.create( account: alix, - options: options + options: ClientOptions.init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db1" + ) ) - try alixClient2.dropLocalDatabaseConnection() - try alixClient2.deleteLocalDatabase() let alixClient3 = try await Client.create( account: alix, - options: options + options: ClientOptions.init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db2" + ) ) let state = try await alixClient3.inboxState(refreshFromNetwork: true) @@ -532,7 +535,8 @@ class ClientTests: XCTestCase { print("PERF: Built a client with inboxId in \(time3)s") // Measure time to build a client with an inboxId and apiClient - try await Client.connectToApiBackend(api: ClientOptions.Api(env: .dev, isSecure: true)) + try await Client.connectToApiBackend( + api: ClientOptions.Api(env: .dev, isSecure: true)) let start4 = Date() try await Client.create( account: fakeWallet, diff --git a/Tests/XMTPTests/ConversationTests.swift b/Tests/XMTPTests/ConversationTests.swift index 69c81326..430ea4bd 100644 --- a/Tests/XMTPTests/ConversationTests.swift +++ b/Tests/XMTPTests/ConversationTests.swift @@ -189,123 +189,4 @@ class ConversationTests: XCTestCase { await fulfillment(of: [expectation1], timeout: 3) } - - func testSyncConsent() async throws { - let fixtures = try await fixtures() - - let key = try Crypto.secureRandomBytes(count: 32) - let alix = try PrivateKey.generate() - var alixClient = try await Client.create( - account: alix, - options: .init( - api: .init(env: .local, isSecure: false), - dbEncryptionKey: key, - dbDirectory: "xmtp_db" - ) - ) - - let dm = try await alixClient.conversations.findOrCreateDm( - with: fixtures.bo.walletAddress) - try await dm.updateConsentState(state: .denied) - XCTAssertEqual(try dm.consentState(), .denied) - - try await fixtures.boClient.conversations.sync() - let boDm = try await fixtures.boClient.findConversation(conversationId: dm.id) - - var alixClient2 = try await Client.create( - account: alix, - options: .init( - api: .init(env: .local, isSecure: false), - dbEncryptionKey: key, - dbDirectory: "xmtp_db2" - ) - ) - - let state = try await alixClient2.inboxState(refreshFromNetwork: true) - XCTAssertEqual(state.installations.count, 2) - - try await fixtures.boClient.conversations.sync() - try await boDm?.sync() - try await alixClient2.preferences.syncConsent() - try await alixClient.conversations.syncAllConversations() - sleep(2) - try await alixClient2.conversations.syncAllConversations() - sleep(2) - - if let dm2 = try await alixClient2.findConversation(conversationId: dm.id) { - XCTAssertEqual(try dm2.consentState(), .denied) - - try await alixClient2.preferences.setConsentState( - entries: [ - ConsentRecord( - value: dm2.id, - entryType: .conversation_id, - consentType: .allowed - ) - ] - ) - let convoState = try await alixClient2.preferences - .conversationState( - conversationId: dm2.id) - XCTAssertEqual(convoState, .allowed) - XCTAssertEqual(try dm2.consentState(), .allowed) - } - } - - func testStreamConsent() async throws { - let fixtures = try await fixtures() - - let key = try Crypto.secureRandomBytes(count: 32) - let alix = try PrivateKey.generate() - - let alixClient = try await Client.create( - account: alix, - options: .init( - api: .init(env: .local, isSecure: false), - dbEncryptionKey: key, - dbDirectory: "xmtp_db" - ) - ) - - let alixGroup = try await alixClient.conversations.newGroup(with: [fixtures.bo.walletAddress]) - - let alixClient2 = try await Client.create( - account: alix, - options: .init( - api: .init(env: .local, isSecure: false), - dbEncryptionKey: key, - dbDirectory: "xmtp_db2" - ) - ) - - try await alixGroup.send(content: "Hello") - try await alixClient.conversations.syncAllConversations() - try await alixClient2.conversations.syncAllConversations() - let alixGroup2 = try alixClient2.findGroup(groupId: alixGroup.id)! - - var consentList = [ConsentRecord]() - let expectation = XCTestExpectation(description: "Stream Consent") - expectation.expectedFulfillmentCount = 3 - - Task(priority: .userInitiated) { - for try await entry in await alixClient.preferences.streamConsent() { - consentList.append(entry) - expectation.fulfill() - } - } - sleep(1) - try await alixGroup2.updateConsentState(state: .denied) - let dm = try await alixClient2.conversations.newConversation(with: fixtures.caro.walletAddress) - try await dm.updateConsentState(state: .denied) - - sleep(5) - try await alixClient.conversations.syncAllConversations() - try await alixClient2.conversations.syncAllConversations() - - await fulfillment(of: [expectation], timeout: 3) - print(consentList) - XCTAssertEqual(try alixGroup.consentState(), .denied) - } - - } diff --git a/Tests/XMTPTests/HistorySyncTests.swift b/Tests/XMTPTests/HistorySyncTests.swift new file mode 100644 index 00000000..6d29a2b0 --- /dev/null +++ b/Tests/XMTPTests/HistorySyncTests.swift @@ -0,0 +1,170 @@ +// +// HistorySyncTests.swift +// XMTPiOS +// +// Created by Naomi Plasterer on 12/19/24. +// + +import Foundation +import XCTest + +@testable import XMTPiOS + +@available(iOS 15, *) +class HistorySyncTests: XCTestCase { + func testSyncConsent() async throws { + let fixtures = try await fixtures() + + let key = try Crypto.secureRandomBytes(count: 32) + let alix = try PrivateKey.generate() + let alixClient = try await Client.create( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db" + ) + ) + + let group = try await alixClient.conversations.newGroup( + with: [fixtures.bo.walletAddress]) + try await group.updateConsentState(state: .denied) + XCTAssertEqual(try group.consentState(), .denied) + + + let alixClient2 = try await Client.create( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db2" + ) + ) + + let state = try await alixClient2.inboxState(refreshFromNetwork: true) + XCTAssertEqual(state.installations.count, 2) + + try await alixClient.conversations.syncAllConversations() + sleep(2) + try await alixClient2.conversations.syncAllConversations() + sleep(2) + + if let dm2 = try await alixClient2.findConversation(conversationId: group.id) { + XCTAssertEqual(try dm2.consentState(), .denied) + + try await alixClient2.preferences.setConsentState( + entries: [ + ConsentRecord( + value: dm2.id, + entryType: .conversation_id, + consentType: .allowed + ) + ] + ) + let convoState = try await alixClient2.preferences + .conversationState( + conversationId: dm2.id) + XCTAssertEqual(convoState, .allowed) + XCTAssertEqual(try dm2.consentState(), .allowed) + } + } + + func testSyncMessages() async throws { + let fixtures = try await fixtures() + + let key = try Crypto.secureRandomBytes(count: 32) + let alix = try PrivateKey.generate() + let alixClient = try await Client.create( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db" + ) + ) + + let group = try await alixClient.conversations.newGroup( + with: [fixtures.bo.walletAddress]) + try await group.send(content: "hi") + let messageCount = try await group.messages().count + XCTAssertEqual(messageCount, 2) + + let alixClient2 = try await Client.create( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db2" + ) + ) + + let state = try await alixClient2.inboxState(refreshFromNetwork: true) + XCTAssertEqual(state.installations.count, 2) + + try await alixClient.conversations.syncAllConversations() + sleep(2) + try await alixClient2.conversations.syncAllConversations() + sleep(2) + + if let dm2 = try await alixClient2.findConversation(conversationId: group.id) { + let messageCount = try await group.messages().count + XCTAssertEqual(messageCount, 2) + } + } + + + func testStreamConsent() async throws { + let fixtures = try await fixtures() + + let key = try Crypto.secureRandomBytes(count: 32) + let alix = try PrivateKey.generate() + + let alixClient = try await Client.create( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db" + ) + ) + + let alixGroup = try await alixClient.conversations.newGroup(with: [fixtures.bo.walletAddress]) + + let alixClient2 = try await Client.create( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db2" + ) + ) + + try await alixGroup.send(content: "Hello") + try await alixClient.conversations.syncAllConversations() + try await alixClient2.conversations.syncAllConversations() + let alixGroup2 = try alixClient2.findGroup(groupId: alixGroup.id)! + + var consentList = [ConsentRecord]() + let expectation = XCTestExpectation(description: "Stream Consent") + expectation.expectedFulfillmentCount = 3 + + Task(priority: .userInitiated) { + for try await entry in await alixClient.preferences.streamConsent() { + consentList.append(entry) + expectation.fulfill() + } + } + sleep(1) + try await alixGroup2.updateConsentState(state: .denied) + let dm = try await alixClient2.conversations.newConversation(with: fixtures.caro.walletAddress) + try await dm.updateConsentState(state: .denied) + + sleep(5) + try await alixClient.conversations.syncAllConversations() + try await alixClient2.conversations.syncAllConversations() + + await fulfillment(of: [expectation], timeout: 3) + print(consentList) + XCTAssertEqual(try alixGroup.consentState(), .denied) + } +} From 07e21ab81e6667c388a16118b3bb3a09d756c7e7 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 20 Dec 2024 08:49:26 -0800 Subject: [PATCH 4/4] bring back sync consent --- Sources/XMTPiOS/PrivatePreferences.swift | 4 ++++ Tests/XMTPTests/HistorySyncTests.swift | 1 + 2 files changed, 5 insertions(+) diff --git a/Sources/XMTPiOS/PrivatePreferences.swift b/Sources/XMTPiOS/PrivatePreferences.swift index de6b8e43..9c6d9c03 100644 --- a/Sources/XMTPiOS/PrivatePreferences.swift +++ b/Sources/XMTPiOS/PrivatePreferences.swift @@ -83,6 +83,10 @@ public actor PrivatePreferences { ).fromFFI } + public func syncConsent() async throws { + try await ffiClient.sendSyncRequest(kind: .consent) + } + public func streamConsent() -> AsyncThrowingStream { diff --git a/Tests/XMTPTests/HistorySyncTests.swift b/Tests/XMTPTests/HistorySyncTests.swift index 6d29a2b0..5a16d483 100644 --- a/Tests/XMTPTests/HistorySyncTests.swift +++ b/Tests/XMTPTests/HistorySyncTests.swift @@ -44,6 +44,7 @@ class HistorySyncTests: XCTestCase { let state = try await alixClient2.inboxState(refreshFromNetwork: true) XCTAssertEqual(state.installations.count, 2) + try await alixClient2.preferences.syncConsent() try await alixClient.conversations.syncAllConversations() sleep(2) try await alixClient2.conversations.syncAllConversations()