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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ let package = Package(
targets: [
.binaryTarget(
name: "LibXMTPSwiftFFI",
url: "https://github.com/xmtp/libxmtp/releases/download/swift-bindings-1.5.6.fe6e305/LibXMTPSwiftFFI.zip",
checksum: "866a6091be3c85d69d9c0ea1a43f08d44450afd914ceb4c8179116660c23444e"
url: "https://github.com/xmtp/libxmtp/releases/download/swift-bindings-1.6.0-dev.c046238/LibXMTPSwiftFFI.zip",
checksum: "c011e963334de61a3bf7d81164a127443c68486b3722912203a76ad2d0771d5f"
),
.target(
name: "XMTPiOS",
Expand Down
47 changes: 42 additions & 5 deletions Sources/XMTPiOS/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@ public struct ClientOptions {
public var isSecure: Bool = true

public var appVersion: String? = nil

public var gatewayUrl: String? = nil

public init(
env: XMTPEnvironment = .dev, isSecure: Bool = true,
appVersion: String? = nil
appVersion: String? = nil,
gatewayUrl: String? = nil
) {
self.env = env
self.isSecure = isSecure
self.appVersion = appVersion
self.gatewayUrl = gatewayUrl
}
}

Expand Down Expand Up @@ -86,9 +90,30 @@ public struct ClientOptions {
}
}

/// Cache for API clients to avoid creating duplicate connections.
/// Cache keys are constructed from v3Host, gatewayHost, isSecure, and appVersion
/// to ensure proper isolation between different API configurations.
actor ApiClientCache {
private var apiClientCache: [String: XmtpApiClient] = [:]
private var syncApiClientCache: [String: XmtpApiClient] = [:]

/// Creates a cache key from API configuration parameters
/// - Parameters:
/// - v3Host: The v3 host URL
/// - gatewayHost: The gateway host URL (optional)
/// - isSecure: Whether the connection uses TLS
/// - appVersion: The app version string (optional)
/// - Returns: A unique cache key string
static func makeCacheKey(
v3Host: String,
gatewayHost: String?,
isSecure: Bool,
appVersion: String?
) -> String {
let gateway = gatewayHost ?? "none"
let version = appVersion ?? "none"
return "\(v3Host)|\(gateway)|\(isSecure)|\(version)"
}

func getClient(forKey key: String) -> XmtpApiClient? {
return apiClientCache[key]
Expand Down Expand Up @@ -362,7 +387,12 @@ public final class Client {
public static func connectToApiBackend(api: ClientOptions.Api) async throws
-> XmtpApiClient
{
let cacheKey = api.env.url
let cacheKey = ApiClientCache.makeCacheKey(
v3Host: api.env.url,
gatewayHost: api.gatewayUrl,
isSecure: api.isSecure,
appVersion: api.appVersion
)

// Check for an existing connected client
if let cached = await apiCache.getClient(forKey: cacheKey),
Expand All @@ -373,7 +403,8 @@ public final class Client {

// Either not cached or not connected; create new client
let newClient = try await connectToBackend(
host: api.env.url,
v3Host: api.env.url,
gatewayHost: api.gatewayUrl,
isSecure: api.isSecure,
appVersion: api.appVersion
)
Expand All @@ -385,7 +416,12 @@ public final class Client {
async throws
-> XmtpApiClient
{
let cacheKey = api.env.url
let cacheKey = ApiClientCache.makeCacheKey(
v3Host: api.env.url,
gatewayHost: api.gatewayUrl,
isSecure: api.isSecure,
appVersion: api.appVersion
)

// Check for an existing connected client
if let cached = await apiCache.getSyncClient(forKey: cacheKey),
Expand All @@ -396,7 +432,8 @@ public final class Client {

// Either not cached or not connected; create new client
let newClient = try await connectToBackend(
host: api.env.url,
v3Host: api.env.url,
gatewayHost: api.gatewayUrl,
isSecure: api.isSecure,
appVersion: api.appVersion
)
Expand Down
51 changes: 29 additions & 22 deletions Sources/XMTPiOS/Conversation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,21 @@ public enum Conversation: Identifiable, Equatable, Hashable {
}
}

public func prepareMessage(encodedContent: EncodedContent) async throws
-> String
{
switch self {
case let .group(group):
return try await group.prepareMessage(
encodedContent: encodedContent)
case let .dm(dm):
return try await dm.prepareMessage(encodedContent: encodedContent)
}
}
public func prepareMessage(
encodedContent: EncodedContent, visibilityOptions: MessageVisibilityOptions? = nil
) async throws -> String
{
switch self {
case let .group(group):
return try await group.prepareMessage(
encodedContent: encodedContent, visibilityOptions: visibilityOptions
)
case let .dm(dm):
return try await dm.prepareMessage(
encodedContent: encodedContent, visibilityOptions: visibilityOptions
)
}
}

public func prepareMessage<T>(content: T, options: SendOptions? = nil)
async throws -> String
Expand Down Expand Up @@ -218,17 +222,20 @@ public enum Conversation: Identifiable, Equatable, Hashable {
}
}

@discardableResult public func send(
encodedContent: EncodedContent
) async throws -> String {
switch self {
case let .group(group):
return try await group.send(
encodedContent: encodedContent)
case let .dm(dm):
return try await dm.send(encodedContent: encodedContent)
}
}
@discardableResult public func send(
encodedContent: EncodedContent, visibilityOptions: MessageVisibilityOptions? = nil
) async throws -> String {
switch self {
case let .group(group):
return try await group.send(
encodedContent: encodedContent, visibilityOptions: visibilityOptions
)
case let .dm(dm):
return try await dm.send(
encodedContent: encodedContent, visibilityOptions: visibilityOptions
)
}
}

public func send(text: String, options: SendOptions? = nil) async throws
-> String
Expand Down
51 changes: 39 additions & 12 deletions Sources/XMTPiOS/Dm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,19 +147,25 @@ public struct Dm: Identifiable, Equatable, Hashable {
public func send<T>(content: T, options: SendOptions? = nil) async throws
-> String
{
let encodeContent = try await encodeContent(
content: content, options: options)
return try await send(encodedContent: encodeContent)
let (encodeContent, visibilityOptions) = try await encodeContent(
content: content, options: options
)
return try await send(encodedContent: encodeContent, visibilityOptions: visibilityOptions)
}

public func send(encodedContent: EncodedContent) async throws -> String {
public func send(
encodedContent: EncodedContent, visibilityOptions: MessageVisibilityOptions? = nil
) async throws -> String {
let opts = visibilityOptions?.toFfi() ?? FfiSendMessageOpts(shouldPush: true)
let messageId = try await ffiConversation.send(
contentBytes: encodedContent.serializedData())
contentBytes: encodedContent.serializedData(),
opts: opts
)
return messageId.toHex
}

public func encodeContent<T>(content: T, options: SendOptions?) async throws
-> EncodedContent
-> (EncodedContent, MessageVisibilityOptions)
{
let codec = Client.codecRegistry.find(for: options?.contentType)

Expand Down Expand Up @@ -193,24 +199,45 @@ public struct Dm: Identifiable, Equatable, Hashable {
encoded = try encoded.compress(compression)
}

return encoded
func shouldPush<Codec: ContentCodec>(codec: Codec, content: Any) throws
-> Bool
{
if let content = content as? Codec.T {
return try codec.shouldPush(content: content)
} else {
throw CodecError.invalidContent
}
}

let visibilityOptions = try MessageVisibilityOptions(
shouldPush: shouldPush(codec: codec, content: content)
)

return (encoded, visibilityOptions)
}

public func prepareMessage(encodedContent: EncodedContent) async throws
public func prepareMessage(
encodedContent: EncodedContent, visibilityOptions: MessageVisibilityOptions? = nil
) async throws
-> String
{
let opts = visibilityOptions?.toFfi() ?? FfiSendMessageOpts(shouldPush: true)
let messageId = try ffiConversation.sendOptimistic(
contentBytes: encodedContent.serializedData())
contentBytes: encodedContent.serializedData(),
opts: opts
)
return messageId.toHex
}

public func prepareMessage<T>(content: T, options: SendOptions? = nil)
async throws -> String
{
let encodeContent = try await encodeContent(
content: content, options: options)
let (encodeContent, visibilityOptions) = try await encodeContent(
content: content, options: options
)
return try ffiConversation.sendOptimistic(
contentBytes: try encodeContent.serializedData()
contentBytes: encodeContent.serializedData(),
opts: visibilityOptions.toFfi()
).toHex
}

Expand Down
51 changes: 39 additions & 12 deletions Sources/XMTPiOS/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,23 +322,29 @@ public struct Group: Identifiable, Equatable, Hashable {
public func send<T>(content: T, options: SendOptions? = nil) async throws
-> String
{
let encodeContent = try await encodeContent(
content: content, options: options)
return try await send(encodedContent: encodeContent)
let (encodeContent, visibilityOptions) = try await encodeContent(
content: content, options: options
)
return try await send(encodedContent: encodeContent, visibilityOptions: visibilityOptions)
}

public func send(encodedContent: EncodedContent) async throws -> String {
public func send(
encodedContent: EncodedContent, visibilityOptions: MessageVisibilityOptions? = nil
) async throws -> String {
do {
let opts = visibilityOptions?.toFfi() ?? FfiSendMessageOpts(shouldPush: true)
let messageId = try await ffiGroup.send(
contentBytes: encodedContent.serializedData())
contentBytes: encodedContent.serializedData(),
opts: opts
)
return messageId.toHex
} catch {
throw error
}
}

public func encodeContent<T>(content: T, options: SendOptions?) async throws
-> EncodedContent
-> (EncodedContent, MessageVisibilityOptions)
{
let codec = Client.codecRegistry.find(for: options?.contentType)

Expand Down Expand Up @@ -372,24 +378,45 @@ public struct Group: Identifiable, Equatable, Hashable {
encoded = try encoded.compress(compression)
}

return encoded
func shouldPush<Codec: ContentCodec>(codec: Codec, content: Any) throws
-> Bool
{
if let content = content as? Codec.T {
return try codec.shouldPush(content: content)
} else {
throw CodecError.invalidContent
}
}

let visibilityOptions = try MessageVisibilityOptions(
shouldPush: shouldPush(codec: codec, content: content)
)

return (encoded, visibilityOptions)
}

public func prepareMessage(encodedContent: EncodedContent) async throws
public func prepareMessage(
encodedContent: EncodedContent, visibilityOptions: MessageVisibilityOptions? = nil
) async throws
-> String
{
let opts = visibilityOptions?.toFfi() ?? FfiSendMessageOpts(shouldPush: true)
let messageId = try ffiGroup.sendOptimistic(
contentBytes: encodedContent.serializedData())
contentBytes: encodedContent.serializedData(),
opts: opts
)
return messageId.toHex
}

public func prepareMessage<T>(content: T, options: SendOptions? = nil)
async throws -> String
{
let encodeContent = try await encodeContent(
content: content, options: options)
let (encodeContent, visibilityOptions) = try await encodeContent(
content: content, options: options
)
return try ffiGroup.sendOptimistic(
contentBytes: try encodeContent.serializedData()
contentBytes: encodeContent.serializedData(),
opts: visibilityOptions.toFfi()
).toHex
}

Expand Down
Loading
Loading