diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 8af6fd7b..52787587 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -8,6 +8,7 @@ public enum ClientError: Error, CustomStringConvertible, LocalizedError { case creationError(String) case missingInboxId case invalidInboxId(String) + case clientDeallocated public var description: String { switch self { @@ -18,6 +19,8 @@ public enum ClientError: Error, CustomStringConvertible, LocalizedError { case let .invalidInboxId(inboxId): return "Invalid inboxId: \(inboxId). Inbox IDs cannot start with '0x'." + case .clientDeallocated: + return "ClientError.clientDeallocated: The Client has been deallocated." } } @@ -154,7 +157,7 @@ struct ApiCacheKey { } actor ApiClientCache { - private var apiClientCache: [String: XmtpApiClient] = [:] + private var apiClientCache: [String: XmtpApiClient] = [:] private var syncApiClientCache: [String: XmtpApiClient] = [:] func getClient(forKey key: String) -> XmtpApiClient? { @@ -162,7 +165,7 @@ actor ApiClientCache { } func setClient(_ client: XmtpApiClient, forKey key: String) { - apiClientCache[key] = client + apiClientCache[key] = client } func getSyncClient(forKey key: String) -> XmtpApiClient? { @@ -170,7 +173,7 @@ actor ApiClientCache { } func setSyncClient(_ client: XmtpApiClient, forKey key: String) { - syncApiClientCache[key] = client + syncApiClientCache[key] = client } } @@ -192,11 +195,11 @@ public final class Client { ) public lazy var preferences: PrivatePreferences = .init( - client: self, ffiClient: ffiClient + ffiClient: ffiClient ) public lazy var debugInformation: XMTPDebugInformation = .init( - client: self, ffiClient: ffiClient + historySyncUrl: environment.getHistorySyncUrl(), ffiClient: ffiClient ) static var codecRegistry = CodecRegistry() diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index 20480d2b..f4b5d0fc 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -123,7 +123,7 @@ actor FfiStreamActor { /// Handles listing and creating Conversations. public class Conversations { - var client: Client + private weak var client: Client? var ffiConversations: FfiConversations var ffiClient: FfiXmtpClient @@ -136,6 +136,13 @@ public class Conversations { self.ffiClient = ffiClient } + private func requireClient() throws -> Client { + guard let client = client else { + throw ClientError.clientDeallocated + } + return client + } + /// Helper function to convert DisappearingMessageSettings to FfiMessageDisappearingSettings /// Returns nil if the input is nil, making it explicit that nil will be passed to FFI private func toFfiDisappearingMessageSettings(_ settings: DisappearingMessageSettings?) @@ -149,6 +156,7 @@ public class Conversations { } public func findGroup(groupId: String) throws -> Group? { + let client = try requireClient() do { return try Group( ffiGroup: ffiClient.conversation( @@ -164,6 +172,7 @@ public class Conversations { public func findConversation(conversationId: String) async throws -> Conversation? { + let client = try requireClient() do { let conversation = try ffiClient.conversation( conversationId: conversationId.hexToData @@ -177,6 +186,7 @@ public class Conversations { public func findConversationByTopic(topic: String) async throws -> Conversation? { + let client = try requireClient() do { let regexPattern = #"/xmtp/mls/1/g-(.*?)/proto"# if let regex = try? NSRegularExpression(pattern: regexPattern) { @@ -200,6 +210,7 @@ public class Conversations { } public func findDmByInboxId(inboxId: InboxId) throws -> Dm? { + let client = try requireClient() do { let conversation = try ffiClient.dmConversation( targetInboxId: inboxId @@ -215,6 +226,7 @@ public class Conversations { public func findDmByIdentity(publicIdentity: PublicIdentity) async throws -> Dm? { + let client = try requireClient() guard let inboxId = try await client.inboxIdFromIdentity( identity: publicIdentity @@ -288,6 +300,7 @@ public class Conversations { if let limit { options.limit = Int64(limit) } + let client = try requireClient() let conversations = try ffiConversations.listGroups( opts: options ) @@ -321,6 +334,7 @@ public class Conversations { options.limit = Int64(limit) } + let client = try requireClient() let conversations = try ffiConversations.listDms( opts: options ) @@ -353,6 +367,7 @@ public class Conversations { if let limit { options.limit = Int64(limit) } + let client = try requireClient() let ffiConversations = try ffiConversations.list( opts: options ) @@ -373,6 +388,10 @@ public class Conversations { Conversation, Error > { AsyncThrowingStream { continuation in + guard let client = self.client else { + continuation.finish(throwing: ClientError.clientDeallocated) + return + } let ffiStreamActor = FfiStreamActor() let conversationCallback = ConversationStreamCallback { conversation in @@ -387,14 +406,14 @@ public class Conversations { if conversationType == .dm { continuation.yield( Conversation.dm( - conversation.dmFromFFI(client: self.client) + conversation.dmFromFFI(client: client) ) ) } else if conversationType == .group { continuation.yield( Conversation.group( conversation.groupFromFFI( - client: self.client + client: client ) ) ) @@ -456,6 +475,7 @@ public class Conversations { with peerIdentity: PublicIdentity, disappearingMessageSettings: DisappearingMessageSettings? = nil ) async throws -> Dm { + let client = try requireClient() if try await client.inboxState(refreshFromNetwork: false).identities .map(\.identifier).contains(peerIdentity.identifier) { @@ -493,6 +513,7 @@ public class Conversations { ) async throws -> Dm { + let client = try requireClient() if peerInboxId == client.inboxID { throw ConversationError.memberCannotBeSelf } @@ -567,6 +588,7 @@ public class Conversations { disappearingMessageSettings: DisappearingMessageSettings? = nil, appData: String? ) async throws -> Group { + let client = try requireClient() let group = try await ffiConversations.createGroup( accountIdentities: identities.map(\.ffiPrivate), opts: FfiCreateGroupOptions( @@ -641,6 +663,7 @@ public class Conversations { disappearingMessageSettings: DisappearingMessageSettings? = nil, appData: String? ) async throws -> Group { + let client = try requireClient() try validateInboxIds(inboxIds) let group = try await ffiConversations.createGroupWithInboxIds( inboxIds: inboxIds, @@ -667,6 +690,7 @@ public class Conversations { disappearingMessageSettings: DisappearingMessageSettings? = nil, appData: String? = nil ) throws -> Group { + let client = try requireClient() let ffiOpts = FfiCreateGroupOptions( permissions: GroupPermissionPreconfiguration.toFfiGroupPermissionOptions( @@ -786,6 +810,7 @@ public class Conversations { public func fromWelcome(envelopeBytes: Data) async throws -> Conversation? { + let client = try requireClient() let conversations = try await ffiConversations .processStreamedWelcomeMessage(envelopeBytes: envelopeBytes) diff --git a/Sources/XMTPiOS/Libxmtp/XMTPDebugInformation.swift b/Sources/XMTPiOS/Libxmtp/XMTPDebugInformation.swift index 9a1970f0..3ce6debb 100644 --- a/Sources/XMTPiOS/Libxmtp/XMTPDebugInformation.swift +++ b/Sources/XMTPiOS/Libxmtp/XMTPDebugInformation.swift @@ -8,11 +8,11 @@ import Foundation public class XMTPDebugInformation { - private let client: Client + private let historySyncUrl: String private let ffiClient: FfiXmtpClient - public init(client: Client, ffiClient: FfiXmtpClient) { - self.client = client + public init(historySyncUrl: String, ffiClient: FfiXmtpClient) { + self.historySyncUrl = historySyncUrl self.ffiClient = ffiClient } @@ -31,6 +31,11 @@ public class XMTPDebugInformation { public func clearAllStatistics() { ffiClient.clearAllStatistics() } + + public func uploadDebugInformation(serverUrl: String? = nil) async throws -> String { + let url = serverUrl ?? historySyncUrl + return try await ffiClient.uploadDebugArchive(serverUrl: url) + } } public class ApiStats { diff --git a/Sources/XMTPiOS/PrivatePreferences.swift b/Sources/XMTPiOS/PrivatePreferences.swift index 42f3ca8b..5207d345 100644 --- a/Sources/XMTPiOS/PrivatePreferences.swift +++ b/Sources/XMTPiOS/PrivatePreferences.swift @@ -47,11 +47,9 @@ public struct ConsentRecord: Codable, Hashable { /// Provides access to contact bundles. public actor PrivatePreferences { - var client: Client var ffiClient: FfiXmtpClient - init(client: Client, ffiClient: FfiXmtpClient) { - self.client = client + init(ffiClient: FfiXmtpClient) { self.ffiClient = ffiClient } @@ -93,22 +91,24 @@ public actor PrivatePreferences { AsyncThrowingStream { continuation in let ffiStreamActor = FfiStreamActor() - let consentCallback = ConsentCallback(client: self.client) { - records in - guard !Task.isCancelled else { - continuation.finish() - Task { - await ffiStreamActor.endStream() + let consentCallback = ConsentCallback( + { records in + guard !Task.isCancelled else { + continuation.finish() + Task { + await ffiStreamActor.endStream() + } + return } - return - } - for consent in records { - continuation.yield(consent.fromFfi) + for consent in records { + continuation.yield(consent.fromFfi) + } + }, + onClose: { + onClose?() + continuation.finish() } - } onClose: { - onClose?() - continuation.finish() - } + ) let task = Task { let stream = await ffiClient.conversations().streamConsent( @@ -132,24 +132,26 @@ public actor PrivatePreferences { AsyncThrowingStream { continuation in let ffiStreamActor = FfiStreamActor() - let preferenceCallback = PreferenceCallback(client: self.client) { - records in - guard !Task.isCancelled else { - continuation.finish() - Task { - await ffiStreamActor.endStream() + let preferenceCallback = PreferenceCallback( + { records in + guard !Task.isCancelled else { + continuation.finish() + Task { + await ffiStreamActor.endStream() + } + return } - return - } - for preference in records { - if case let .hmac(key) = preference { - continuation.yield(.hmac_keys) + for preference in records { + if case let .hmac(key) = preference { + continuation.yield(.hmac_keys) + } } + }, + onClose: { + onClose?() + continuation.finish() } - } onClose: { - onClose?() - continuation.finish() - } + ) let task = Task { let stream = await ffiClient.conversations().streamPreferences( @@ -169,15 +171,13 @@ public actor PrivatePreferences { } final class ConsentCallback: FfiConsentCallback { - let client: Client let callback: ([FfiConsent]) -> Void let onCloseCallback: () -> Void init( - client: Client, _ callback: @escaping ([FfiConsent]) -> Void, + _ callback: @escaping ([FfiConsent]) -> Void, onClose: @escaping () -> Void ) { - self.client = client self.callback = callback onCloseCallback = onClose } @@ -196,15 +196,13 @@ final class ConsentCallback: FfiConsentCallback { } final class PreferenceCallback: FfiPreferenceCallback { - let client: Client let callback: ([FfiPreferenceUpdate]) -> Void let onCloseCallback: () -> Void init( - client: Client, _ callback: @escaping ([FfiPreferenceUpdate]) -> Void, + _ callback: @escaping ([FfiPreferenceUpdate]) -> Void, onClose: @escaping () -> Void ) { - self.client = client self.callback = callback onCloseCallback = onClose }