From 1099e98fbff66b4081cadf7e77244490aabf7688 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 08:20:30 +0000 Subject: [PATCH 1/5] chore: update Apple SDK to 16.1.0 --- README.md | 4 ++-- Sources/Appwrite/Client.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b218630..ddd19f5 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ ![Swift Package Manager](https://img.shields.io/github/v/release/appwrite/sdk-for-apple.svg?color=green&style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-apple.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.1-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.0-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK is compatible with Appwrite server version 1.9.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-apple/releases).** +**This SDK is compatible with Appwrite server version latest. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-apple/releases).** Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Apple SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) diff --git a/Sources/Appwrite/Client.swift b/Sources/Appwrite/Client.swift index 308d378..fb52306 100644 --- a/Sources/Appwrite/Client.swift +++ b/Sources/Appwrite/Client.swift @@ -27,7 +27,7 @@ open class Client { "x-sdk-platform": "client", "x-sdk-language": "apple", "x-sdk-version": "16.1.0", - "x-appwrite-response-format": "1.9.1" + "x-appwrite-response-format": "1.9.0" ] internal var config: [String: String] = [:] From 7f65d2d7c31bea304b0439f9060c87a6a1615c8f Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 08:32:17 +0000 Subject: [PATCH 2/5] chore: update Apple SDK to 16.2.0 --- CHANGELOG.md | 6 + README.md | 4 +- Sources/Appwrite/Client.swift | 18 +-- Sources/Appwrite/Services/Account.swift | 102 ++++++++-------- Sources/Appwrite/Services/Avatars.swift | 2 +- Sources/Appwrite/Services/Databases.swift | 26 ++-- Sources/Appwrite/Services/Functions.swift | 8 +- Sources/Appwrite/Services/Graphql.swift | 6 +- Sources/Appwrite/Services/Locale.swift | 18 +-- Sources/Appwrite/Services/Messaging.swift | 4 +- Sources/Appwrite/Services/Realtime.swift | 140 ++++++++++++++-------- Sources/Appwrite/Services/Storage.swift | 10 +- Sources/Appwrite/Services/TablesDB.swift | 26 ++-- Sources/Appwrite/Services/Teams.swift | 24 ++-- 14 files changed, 225 insertions(+), 169 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bcfb0..2728ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 16.2.0 + +* Added: `Client.call()` converters now supported throwing parse transformations +* Fixed: Multipart upload conversion now propagated converter errors correctly +* Updated: README compatibility note now targets `latest` server version + ## 16.1.0 * Added `x` OAuth provider to `OAuthProvider` enum diff --git a/README.md b/README.md index ddd19f5..f370aff 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Swift Package Manager](https://img.shields.io/github/v/release/appwrite/sdk-for-apple.svg?color=green&style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-apple.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.1-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) @@ -31,7 +31,7 @@ Add the package to your `Package.swift` dependencies: ```swift dependencies: [ - .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "16.1.0"), + .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "16.2.0"), ], ``` diff --git a/Sources/Appwrite/Client.swift b/Sources/Appwrite/Client.swift index fb52306..d798631 100644 --- a/Sources/Appwrite/Client.swift +++ b/Sources/Appwrite/Client.swift @@ -26,8 +26,8 @@ open class Client { "x-sdk-name": "Apple", "x-sdk-platform": "client", "x-sdk-language": "apple", - "x-sdk-version": "16.1.0", - "x-appwrite-response-format": "1.9.0" + "x-sdk-version": "16.2.0", + "x-appwrite-response-format": "1.9.1" ] internal var config: [String: String] = [:] @@ -355,7 +355,7 @@ open class Client { headers: [String: String] = [:], params: [String: Any?] = [:], sink: ((ByteBuffer) -> Void)? = nil, - converter: ((Any) -> T)? = nil + converter: ((Any) throws -> T)? = nil ) async throws -> T { let validParams = params.filter { $0.value != nil } @@ -395,7 +395,7 @@ open class Client { private func execute( _ request: HTTPClientRequest, withSink bufferSink: ((ByteBuffer) -> Void)? = nil, - converter: ((Any) -> T)? = nil + converter: ((Any) throws -> T)? = nil ) async throws -> T { let response = try await http.execute( request, @@ -431,7 +431,11 @@ open class Client { } let dict = try JSONSerialization.jsonObject(with: Data(data.readableBytesView)) as? [String: Any] - return converter?(dict!) ?? dict! as! T + if let converter = converter { + return try converter(dict!) + } + + return dict! as! T } default: var message = "" @@ -464,7 +468,7 @@ open class Client { params: inout [String: Any?], paramName: String, idParamName: String? = nil, - converter: ((Any) -> T)? = nil, + converter: ((Any) throws -> T)? = nil, onProgress: ((UploadProgress) -> Void)? = nil ) async throws -> T { let input = params[paramName] as! InputFile @@ -537,7 +541,7 @@ open class Client { )) } - return converter!(result) + return try converter!(result) } private static func randomBoundary() -> String { diff --git a/Sources/Appwrite/Services/Account.swift b/Sources/Appwrite/Services/Account.swift index a05909f..1eec944 100644 --- a/Sources/Appwrite/Services/Account.swift +++ b/Sources/Appwrite/Services/Account.swift @@ -23,7 +23,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -86,7 +86,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -163,7 +163,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -225,7 +225,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.IdentityList = { response in + let converter: (Any) throws -> AppwriteModels.IdentityList = { response in return AppwriteModels.IdentityList.from(map: response as! [String: Any]) } @@ -290,7 +290,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Jwt = { response in + let converter: (Any) throws -> AppwriteModels.Jwt = { response in return AppwriteModels.Jwt.from(map: response as! [String: Any]) } @@ -326,7 +326,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.LogList = { response in + let converter: (Any) throws -> AppwriteModels.LogList = { response in return AppwriteModels.LogList.from(map: response as! [String: Any]) } @@ -361,7 +361,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -415,7 +415,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.MfaType = { response in + let converter: (Any) throws -> AppwriteModels.MfaType = { response in return AppwriteModels.MfaType.from(map: response as! [String: Any]) } @@ -451,7 +451,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.MfaType = { response in + let converter: (Any) throws -> AppwriteModels.MfaType = { response in return AppwriteModels.MfaType.from(map: response as! [String: Any]) } @@ -492,7 +492,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -555,7 +555,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -669,7 +669,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.MfaChallenge = { response in + let converter: (Any) throws -> AppwriteModels.MfaChallenge = { response in return AppwriteModels.MfaChallenge.from(map: response as! [String: Any]) } @@ -705,7 +705,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.MfaChallenge = { response in + let converter: (Any) throws -> AppwriteModels.MfaChallenge = { response in return AppwriteModels.MfaChallenge.from(map: response as! [String: Any]) } @@ -747,7 +747,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -788,7 +788,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -816,7 +816,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.MfaFactors = { response in + let converter: (Any) throws -> AppwriteModels.MfaFactors = { response in return AppwriteModels.MfaFactors.from(map: response as! [String: Any]) } @@ -843,7 +843,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.MfaFactors = { response in + let converter: (Any) throws -> AppwriteModels.MfaFactors = { response in return AppwriteModels.MfaFactors.from(map: response as! [String: Any]) } @@ -874,7 +874,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.MfaRecoveryCodes = { response in + let converter: (Any) throws -> AppwriteModels.MfaRecoveryCodes = { response in return AppwriteModels.MfaRecoveryCodes.from(map: response as! [String: Any]) } @@ -904,7 +904,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.MfaRecoveryCodes = { response in + let converter: (Any) throws -> AppwriteModels.MfaRecoveryCodes = { response in return AppwriteModels.MfaRecoveryCodes.from(map: response as! [String: Any]) } @@ -938,7 +938,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.MfaRecoveryCodes = { response in + let converter: (Any) throws -> AppwriteModels.MfaRecoveryCodes = { response in return AppwriteModels.MfaRecoveryCodes.from(map: response as! [String: Any]) } @@ -971,7 +971,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.MfaRecoveryCodes = { response in + let converter: (Any) throws -> AppwriteModels.MfaRecoveryCodes = { response in return AppwriteModels.MfaRecoveryCodes.from(map: response as! [String: Any]) } @@ -1004,7 +1004,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.MfaRecoveryCodes = { response in + let converter: (Any) throws -> AppwriteModels.MfaRecoveryCodes = { response in return AppwriteModels.MfaRecoveryCodes.from(map: response as! [String: Any]) } @@ -1036,7 +1036,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.MfaRecoveryCodes = { response in + let converter: (Any) throws -> AppwriteModels.MfaRecoveryCodes = { response in return AppwriteModels.MfaRecoveryCodes.from(map: response as! [String: Any]) } @@ -1071,7 +1071,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -1128,7 +1128,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -1192,7 +1192,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -1244,7 +1244,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Preferences = { response in + let converter: (Any) throws -> AppwriteModels.Preferences = { response in return AppwriteModels.Preferences.from(map: response as! [String: Any]) } @@ -1294,7 +1294,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -1357,7 +1357,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -1406,7 +1406,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -1434,7 +1434,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.SessionList = { response in + let converter: (Any) throws -> AppwriteModels.SessionList = { response in return AppwriteModels.SessionList.from(map: response as! [String: Any]) } @@ -1493,7 +1493,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -1535,7 +1535,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -1575,7 +1575,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -1675,7 +1675,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -1714,7 +1714,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -1746,7 +1746,7 @@ open class Account: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -1781,7 +1781,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Session = { response in + let converter: (Any) throws -> AppwriteModels.Session = { response in return AppwriteModels.Session.from(map: response as! [String: Any]) } @@ -1844,7 +1844,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in + let converter: (Any) throws -> AppwriteModels.User = { response in return AppwriteModels.User.from(map: response as! [String: Any]) } @@ -1903,7 +1903,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Target = { response in + let converter: (Any) throws -> AppwriteModels.Target = { response in return AppwriteModels.Target.from(map: response as! [String: Any]) } @@ -1944,7 +1944,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Target = { response in + let converter: (Any) throws -> AppwriteModels.Target = { response in return AppwriteModels.Target.from(map: response as! [String: Any]) } @@ -2026,7 +2026,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2082,7 +2082,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2186,7 +2186,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2234,7 +2234,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2283,7 +2283,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2323,7 +2323,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2364,7 +2364,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2400,7 +2400,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2440,7 +2440,7 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Token = { response in + let converter: (Any) throws -> AppwriteModels.Token = { response in return AppwriteModels.Token.from(map: response as! [String: Any]) } @@ -2454,4 +2454,4 @@ open class Account: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/Avatars.swift b/Sources/Appwrite/Services/Avatars.swift index 7ec0d6a..a029bb2 100644 --- a/Sources/Appwrite/Services/Avatars.swift +++ b/Sources/Appwrite/Services/Avatars.swift @@ -382,4 +382,4 @@ open class Avatars: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/Databases.swift b/Sources/Appwrite/Services/Databases.swift index 989e457..2ab5617 100644 --- a/Sources/Appwrite/Services/Databases.swift +++ b/Sources/Appwrite/Services/Databases.swift @@ -27,7 +27,7 @@ open class Databases: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.TransactionList = { response in + let converter: (Any) throws -> AppwriteModels.TransactionList = { response in return AppwriteModels.TransactionList.from(map: response as! [String: Any]) } @@ -61,7 +61,7 @@ open class Databases: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Transaction = { response in + let converter: (Any) throws -> AppwriteModels.Transaction = { response in return AppwriteModels.Transaction.from(map: response as! [String: Any]) } @@ -92,7 +92,7 @@ open class Databases: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Transaction = { response in + let converter: (Any) throws -> AppwriteModels.Transaction = { response in return AppwriteModels.Transaction.from(map: response as! [String: Any]) } @@ -132,7 +132,7 @@ open class Databases: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Transaction = { response in + let converter: (Any) throws -> AppwriteModels.Transaction = { response in return AppwriteModels.Transaction.from(map: response as! [String: Any]) } @@ -196,7 +196,7 @@ open class Databases: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Transaction = { response in + let converter: (Any) throws -> AppwriteModels.Transaction = { response in return AppwriteModels.Transaction.from(map: response as! [String: Any]) } @@ -246,7 +246,7 @@ open class Databases: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.DocumentList = { response in + let converter: (Any) throws -> AppwriteModels.DocumentList = { response in return AppwriteModels.DocumentList.from(map: response as! [String: Any]) } @@ -334,7 +334,7 @@ open class Databases: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Document = { response in + let converter: (Any) throws -> AppwriteModels.Document = { response in return AppwriteModels.Document.from(map: response as! [String: Any]) } @@ -417,7 +417,7 @@ open class Databases: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Document = { response in + let converter: (Any) throws -> AppwriteModels.Document = { response in return AppwriteModels.Document.from(map: response as! [String: Any]) } @@ -502,7 +502,7 @@ open class Databases: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Document = { response in + let converter: (Any) throws -> AppwriteModels.Document = { response in return AppwriteModels.Document.from(map: response as! [String: Any]) } @@ -590,7 +590,7 @@ open class Databases: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Document = { response in + let converter: (Any) throws -> AppwriteModels.Document = { response in return AppwriteModels.Document.from(map: response as! [String: Any]) } @@ -716,7 +716,7 @@ open class Databases: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Document = { response in + let converter: (Any) throws -> AppwriteModels.Document = { response in return AppwriteModels.Document.from(map: response as! [String: Any]) } @@ -806,7 +806,7 @@ open class Databases: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Document = { response in + let converter: (Any) throws -> AppwriteModels.Document = { response in return AppwriteModels.Document.from(map: response as! [String: Any]) } @@ -856,4 +856,4 @@ open class Databases: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/Functions.swift b/Sources/Appwrite/Services/Functions.swift index 63431b6..201b46a 100644 --- a/Sources/Appwrite/Services/Functions.swift +++ b/Sources/Appwrite/Services/Functions.swift @@ -34,7 +34,7 @@ open class Functions: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.ExecutionList = { response in + let converter: (Any) throws -> AppwriteModels.ExecutionList = { response in return AppwriteModels.ExecutionList.from(map: response as! [String: Any]) } @@ -89,7 +89,7 @@ open class Functions: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Execution = { response in + let converter: (Any) throws -> AppwriteModels.Execution = { response in return AppwriteModels.Execution.from(map: response as! [String: Any]) } @@ -123,7 +123,7 @@ open class Functions: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Execution = { response in + let converter: (Any) throws -> AppwriteModels.Execution = { response in return AppwriteModels.Execution.from(map: response as! [String: Any]) } @@ -137,4 +137,4 @@ open class Functions: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/Graphql.swift b/Sources/Appwrite/Services/Graphql.swift index 6089958..3bc2cda 100644 --- a/Sources/Appwrite/Services/Graphql.swift +++ b/Sources/Appwrite/Services/Graphql.swift @@ -30,7 +30,7 @@ open class Graphql: Service { "content-type": "application/json" ] - let converter: (Any) -> Any = { response in + let converter: (Any) throws -> Any = { response in return response } @@ -65,7 +65,7 @@ open class Graphql: Service { "content-type": "application/json" ] - let converter: (Any) -> Any = { response in + let converter: (Any) throws -> Any = { response in return response } @@ -79,4 +79,4 @@ open class Graphql: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/Locale.swift b/Sources/Appwrite/Services/Locale.swift index 87c64c7..5b70743 100644 --- a/Sources/Appwrite/Services/Locale.swift +++ b/Sources/Appwrite/Services/Locale.swift @@ -27,7 +27,7 @@ open class Locale: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Locale = { response in + let converter: (Any) throws -> AppwriteModels.Locale = { response in return AppwriteModels.Locale.from(map: response as! [String: Any]) } @@ -55,7 +55,7 @@ open class Locale: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.LocaleCodeList = { response in + let converter: (Any) throws -> AppwriteModels.LocaleCodeList = { response in return AppwriteModels.LocaleCodeList.from(map: response as! [String: Any]) } @@ -83,7 +83,7 @@ open class Locale: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.ContinentList = { response in + let converter: (Any) throws -> AppwriteModels.ContinentList = { response in return AppwriteModels.ContinentList.from(map: response as! [String: Any]) } @@ -111,7 +111,7 @@ open class Locale: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.CountryList = { response in + let converter: (Any) throws -> AppwriteModels.CountryList = { response in return AppwriteModels.CountryList.from(map: response as! [String: Any]) } @@ -139,7 +139,7 @@ open class Locale: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.CountryList = { response in + let converter: (Any) throws -> AppwriteModels.CountryList = { response in return AppwriteModels.CountryList.from(map: response as! [String: Any]) } @@ -167,7 +167,7 @@ open class Locale: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.PhoneList = { response in + let converter: (Any) throws -> AppwriteModels.PhoneList = { response in return AppwriteModels.PhoneList.from(map: response as! [String: Any]) } @@ -196,7 +196,7 @@ open class Locale: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.CurrencyList = { response in + let converter: (Any) throws -> AppwriteModels.CurrencyList = { response in return AppwriteModels.CurrencyList.from(map: response as! [String: Any]) } @@ -224,7 +224,7 @@ open class Locale: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.LanguageList = { response in + let converter: (Any) throws -> AppwriteModels.LanguageList = { response in return AppwriteModels.LanguageList.from(map: response as! [String: Any]) } @@ -238,4 +238,4 @@ open class Locale: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/Messaging.swift b/Sources/Appwrite/Services/Messaging.swift index d38d417..c25be74 100644 --- a/Sources/Appwrite/Services/Messaging.swift +++ b/Sources/Appwrite/Services/Messaging.swift @@ -35,7 +35,7 @@ open class Messaging: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Subscriber = { response in + let converter: (Any) throws -> AppwriteModels.Subscriber = { response in return AppwriteModels.Subscriber.from(map: response as! [String: Any]) } @@ -79,4 +79,4 @@ open class Messaging: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/Realtime.swift b/Sources/Appwrite/Services/Realtime.swift index ecdf0f1..9f81800 100644 --- a/Sources/Appwrite/Services/Realtime.swift +++ b/Sources/Appwrite/Services/Realtime.swift @@ -8,6 +8,7 @@ open class Realtime : Service { private let TYPE_ERROR = "error" private let TYPE_EVENT = "event" private let TYPE_PONG = "pong" + private let TYPE_RESPONSE = "response" private let DEBOUNCE_NANOS = 1_000_000 private let HEARTBEAT_INTERVAL: UInt64 = 20_000_000_000 // 20 seconds in nanoseconds @@ -19,6 +20,7 @@ open class Realtime : Service { private var slotToSubscriptionId = [Int: String]() // Inverse map: subscriptionId -> slot index (for O(1) lookup) private var subscriptionIdToSlot = [String: Int]() + private var pendingSubscribeSlots = [Int]() private var heartbeatTask: Task? = nil let connectSync = DispatchQueue(label: "ConnectSync") @@ -68,46 +70,13 @@ open class Realtime : Service { } private func createSocket() async throws { - // Rebuild activeChannels from all slots - var allChannels = Set() - for subscription in activeSubscriptions.values { - allChannels.formUnion(subscription.channels) - } - - guard allChannels.count > 0 else { + guard activeSubscriptions.count > 0 else { reconnect = false try await closeSocket() return } - var queryParams = "project=\(client.config["project"]!)" - - for channel in allChannels { - let encodedChannel = channel.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? channel - queryParams += "&channels[]=\(encodedChannel)" - } - - // Build query string from slots β†’ channels β†’ queries - // Format: channel[slot][]=query (each query sent as separate parameter) - // For each slot, repeat its queries under each channel it subscribes to - // Example: slot 1 β†’ channels [tests, prod], queries [q1, q2] - // Produces: tests[1][]=q1&tests[1][]=q2&prod[1][]=q1&prod[1][]=q2 - let selectAllQuery = Query.select(["*"]) - for (slot, subscription) in activeSubscriptions { - // Get queries array - each query is a separate string - let queries = activeSubscriptionQueries[slot] ?? [] - let queryArray = queries.isEmpty ? [selectAllQuery] : queries - - // Repeat this slot's queries under each channel it subscribes to - // Each query is sent as a separate parameter: channel[slot][]=q1&channel[slot][]=q2 - for channel in subscription.channels { - let encodedChannel = channel.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? channel - for query in queryArray { - let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query - queryParams += "&\(encodedChannel)[\(slot)][]=\(encodedQuery)" - } - } - } + let queryParams = "project=\(client.config["project"]!)" let url = "\(client.endPointRealtime!)/realtime?\(queryParams)" @@ -125,6 +94,41 @@ open class Realtime : Service { try await socketClient?.connect() } + private func sendSubscribeMessage() { + guard let ws = socketClient, ws.isConnected else { + return + } + + var rows = [[String: Any]]() + pendingSubscribeSlots.removeAll() + + for (slot, subscription) in activeSubscriptions { + let queries = activeSubscriptionQueries[slot] ?? [] + var row: [String: Any] = [ + "channels": Array(subscription.channels), + "queries": queries + ] + if let knownSubscriptionId = slotToSubscriptionId[slot] { + row["subscriptionId"] = knownSubscriptionId + } + rows.append(row) + pendingSubscribeSlots.append(slot) + } + + if rows.isEmpty { + return + } + + let payload: [String: Any] = [ + "type": "subscribe", + "data": rows + ] + if let data = try? JSONSerialization.data(withJSONObject: payload), + let text = String(data: data, encoding: .utf8) { + ws.send(text: text) + } + } + private func closeSocket() async throws { stopHeartbeat() @@ -243,7 +247,11 @@ open class Realtime : Service { try await Task.sleep(nanoseconds: UInt64(DEBOUNCE_NANOS)) if self.subCallDepth == 1 { - try await self.createSocket() + if let ws = self.socketClient, ws.isConnected { + self.sendSubscribeMessage() + } else { + try await self.createSocket() + } } connectSync.sync { @@ -258,7 +266,16 @@ open class Realtime : Service { if let sid = subscriptionId { self.subscriptionIdToSlot[sid] = nil } - try await self.createSocket() + if self.activeSubscriptions.isEmpty { + self.reconnect = false + try await self.closeSocket() + return + } + if let ws = self.socketClient, ws.isConnected { + self.sendSubscribeMessage() + } else { + try await self.createSocket() + } } } @@ -275,20 +292,47 @@ extension Realtime: WebSocketClientDelegate { } private func handleResponseConnected(from json: [String: Any]) { - guard let data = json["data"] as? [String: Any], - let subscriptions = data["subscriptions"] as? [String: String] else { + guard let data = json["data"] as? [String: Any] else { return } - // Store subscription ID mappings from backend - // Format: { "0": "sub_a1f9", "1": "sub_b83c", ... } - slotToSubscriptionId.removeAll() - subscriptionIdToSlot.removeAll() - for (slotStr, subscriptionId) in subscriptions { - if let slot = Int(slotStr) { - slotToSubscriptionId[slot] = subscriptionId - subscriptionIdToSlot[subscriptionId] = slot + if let subscriptions = data["subscriptions"] as? [String: String] { + // Store subscription ID mappings from backend. + // Try direct slot first, then slot+1 for zero-based server maps. + for (slotStr, subscriptionId) in subscriptions { + if let slot = Int(slotStr) { + let targetSlot: Int + if activeSubscriptions[slot] != nil { + targetSlot = slot + } else if activeSubscriptions[slot + 1] != nil { + targetSlot = slot + 1 + } else { + targetSlot = slot + } + slotToSubscriptionId[targetSlot] = subscriptionId + subscriptionIdToSlot[subscriptionId] = targetSlot + } + } + } + + sendSubscribeMessage() + } + + private func handleResponseAction(from json: [String: Any]) { + guard let data = json["data"] as? [String: Any], + data["to"] as? String == "subscribe", + let subscriptions = data["subscriptions"] as? [[String: Any]] else { + return + } + + for (index, item) in subscriptions.enumerated() { + guard index < pendingSubscribeSlots.count, + let subscriptionId = item["subscriptionId"] as? String else { + continue } + let slot = pendingSubscribeSlots[index] + slotToSubscriptionId[slot] = subscriptionId + subscriptionIdToSlot[subscriptionId] = slot } } @@ -308,6 +352,8 @@ extension Realtime: WebSocketClientDelegate { } case "connected": handleResponseConnected(from: json) + case TYPE_RESPONSE: + handleResponseAction(from: json) case TYPE_EVENT: handleResponseEvent(from: json) case TYPE_PONG: diff --git a/Sources/Appwrite/Services/Storage.swift b/Sources/Appwrite/Services/Storage.swift index 46c7482..22dc185 100644 --- a/Sources/Appwrite/Services/Storage.swift +++ b/Sources/Appwrite/Services/Storage.swift @@ -37,7 +37,7 @@ open class Storage: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.FileList = { response in + let converter: (Any) throws -> AppwriteModels.FileList = { response in return AppwriteModels.FileList.from(map: response as! [String: Any]) } @@ -98,7 +98,7 @@ open class Storage: Service { "content-type": "multipart/form-data" ] - let converter: (Any) -> AppwriteModels.File = { response in + let converter: (Any) throws -> AppwriteModels.File = { response in return AppwriteModels.File.from(map: response as! [String: Any]) } @@ -137,7 +137,7 @@ open class Storage: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.File = { response in + let converter: (Any) throws -> AppwriteModels.File = { response in return AppwriteModels.File.from(map: response as! [String: Any]) } @@ -181,7 +181,7 @@ open class Storage: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.File = { response in + let converter: (Any) throws -> AppwriteModels.File = { response in return AppwriteModels.File.from(map: response as! [String: Any]) } @@ -360,4 +360,4 @@ open class Storage: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/TablesDB.swift b/Sources/Appwrite/Services/TablesDB.swift index 03795f3..c812ccb 100644 --- a/Sources/Appwrite/Services/TablesDB.swift +++ b/Sources/Appwrite/Services/TablesDB.swift @@ -27,7 +27,7 @@ open class TablesDB: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.TransactionList = { response in + let converter: (Any) throws -> AppwriteModels.TransactionList = { response in return AppwriteModels.TransactionList.from(map: response as! [String: Any]) } @@ -61,7 +61,7 @@ open class TablesDB: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Transaction = { response in + let converter: (Any) throws -> AppwriteModels.Transaction = { response in return AppwriteModels.Transaction.from(map: response as! [String: Any]) } @@ -92,7 +92,7 @@ open class TablesDB: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Transaction = { response in + let converter: (Any) throws -> AppwriteModels.Transaction = { response in return AppwriteModels.Transaction.from(map: response as! [String: Any]) } @@ -132,7 +132,7 @@ open class TablesDB: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Transaction = { response in + let converter: (Any) throws -> AppwriteModels.Transaction = { response in return AppwriteModels.Transaction.from(map: response as! [String: Any]) } @@ -196,7 +196,7 @@ open class TablesDB: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Transaction = { response in + let converter: (Any) throws -> AppwriteModels.Transaction = { response in return AppwriteModels.Transaction.from(map: response as! [String: Any]) } @@ -245,7 +245,7 @@ open class TablesDB: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.RowList = { response in + let converter: (Any) throws -> AppwriteModels.RowList = { response in return AppwriteModels.RowList.from(map: response as! [String: Any]) } @@ -331,7 +331,7 @@ open class TablesDB: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Row = { response in + let converter: (Any) throws -> AppwriteModels.Row = { response in return AppwriteModels.Row.from(map: response as! [String: Any]) } @@ -412,7 +412,7 @@ open class TablesDB: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Row = { response in + let converter: (Any) throws -> AppwriteModels.Row = { response in return AppwriteModels.Row.from(map: response as! [String: Any]) } @@ -495,7 +495,7 @@ open class TablesDB: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Row = { response in + let converter: (Any) throws -> AppwriteModels.Row = { response in return AppwriteModels.Row.from(map: response as! [String: Any]) } @@ -581,7 +581,7 @@ open class TablesDB: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Row = { response in + let converter: (Any) throws -> AppwriteModels.Row = { response in return AppwriteModels.Row.from(map: response as! [String: Any]) } @@ -704,7 +704,7 @@ open class TablesDB: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Row = { response in + let converter: (Any) throws -> AppwriteModels.Row = { response in return AppwriteModels.Row.from(map: response as! [String: Any]) } @@ -792,7 +792,7 @@ open class TablesDB: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Row = { response in + let converter: (Any) throws -> AppwriteModels.Row = { response in return AppwriteModels.Row.from(map: response as! [String: Any]) } @@ -841,4 +841,4 @@ open class TablesDB: Service { } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Services/Teams.swift b/Sources/Appwrite/Services/Teams.swift index 54744d9..b40b9b7 100644 --- a/Sources/Appwrite/Services/Teams.swift +++ b/Sources/Appwrite/Services/Teams.swift @@ -35,7 +35,7 @@ open class Teams: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.TeamList = { response in + let converter: (Any) throws -> AppwriteModels.TeamList = { response in return AppwriteModels.TeamList.from(map: response as! [String: Any]) } @@ -102,7 +102,7 @@ open class Teams: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Team = { response in + let converter: (Any) throws -> AppwriteModels.Team = { response in return AppwriteModels.Team.from(map: response as! [String: Any]) } @@ -159,7 +159,7 @@ open class Teams: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Team = { response in + let converter: (Any) throws -> AppwriteModels.Team = { response in return AppwriteModels.Team.from(map: response as! [String: Any]) } @@ -214,7 +214,7 @@ open class Teams: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Team = { response in + let converter: (Any) throws -> AppwriteModels.Team = { response in return AppwriteModels.Team.from(map: response as! [String: Any]) } @@ -305,7 +305,7 @@ open class Teams: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.MembershipList = { response in + let converter: (Any) throws -> AppwriteModels.MembershipList = { response in return AppwriteModels.MembershipList.from(map: response as! [String: Any]) } @@ -377,7 +377,7 @@ open class Teams: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Membership = { response in + let converter: (Any) throws -> AppwriteModels.Membership = { response in return AppwriteModels.Membership.from(map: response as! [String: Any]) } @@ -413,7 +413,7 @@ open class Teams: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Membership = { response in + let converter: (Any) throws -> AppwriteModels.Membership = { response in return AppwriteModels.Membership.from(map: response as! [String: Any]) } @@ -456,7 +456,7 @@ open class Teams: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Membership = { response in + let converter: (Any) throws -> AppwriteModels.Membership = { response in return AppwriteModels.Membership.from(map: response as! [String: Any]) } @@ -537,7 +537,7 @@ open class Teams: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Membership = { response in + let converter: (Any) throws -> AppwriteModels.Membership = { response in return AppwriteModels.Membership.from(map: response as! [String: Any]) } @@ -571,7 +571,7 @@ open class Teams: Service { let apiHeaders: [String: String] = [:] - let converter: (Any) -> AppwriteModels.Preferences = { response in + let converter: (Any) throws -> AppwriteModels.Preferences = { response in return AppwriteModels.Preferences.from(map: response as! [String: Any]) } @@ -630,7 +630,7 @@ open class Teams: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.Preferences = { response in + let converter: (Any) throws -> AppwriteModels.Preferences = { response in return AppwriteModels.Preferences.from(map: response as! [String: Any]) } @@ -666,4 +666,4 @@ open class Teams: Service { } -} \ No newline at end of file +} From 7bc7c9a976fd3a69bf10fd07537d2b116d364a10 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 20 Apr 2026 14:07:08 +0530 Subject: [PATCH 3/5] chore: sync generated SDK for latest Made-with: Cursor --- .github/ISSUE_TEMPLATE/bug.yaml | 82 ----------------------- .github/ISSUE_TEMPLATE/documentation.yaml | 32 --------- .github/ISSUE_TEMPLATE/feature.yaml | 40 ----------- .github/workflows/autoclose.yml | 11 --- 4 files changed, 165 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug.yaml delete mode 100644 .github/ISSUE_TEMPLATE/documentation.yaml delete mode 100644 .github/ISSUE_TEMPLATE/feature.yaml delete mode 100644 .github/workflows/autoclose.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml deleted file mode 100644 index 175c606..0000000 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: "πŸ› Bug Report" -description: "Submit a bug report to help us improve" -title: "πŸ› Bug Report: " -labels: [bug] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out our bug report form πŸ™ - - type: textarea - id: steps-to-reproduce - validations: - required: true - attributes: - label: "πŸ‘Ÿ Reproduction steps" - description: "How do you trigger this bug? Please walk us through it step by step." - placeholder: "When I ..." - - type: textarea - id: expected-behavior - validations: - required: true - attributes: - label: "πŸ‘ Expected behavior" - description: "What did you think would happen?" - placeholder: "It should ..." - - type: textarea - id: actual-behavior - validations: - required: true - attributes: - label: "πŸ‘Ž Actual Behavior" - description: "What did actually happen? Add screenshots, if applicable." - placeholder: "It actually ..." - - type: dropdown - id: appwrite-version - attributes: - label: "🎲 Appwrite version" - description: "What version of Appwrite are you running?" - options: - - Version 0.10.x - - Version 0.9.x - - Version 0.8.x - - Version 0.7.x - - Version 0.6.x - - Different version (specify in environment) - validations: - required: true - - type: dropdown - id: operating-system - attributes: - label: "πŸ’» Operating system" - description: "What OS is your server / device running on?" - options: - - Linux - - MacOS - - Windows - - Something else - validations: - required: true - - type: textarea - id: enviromnemt - validations: - required: false - attributes: - label: "🧱 Your Environment" - description: "Is your environment customized in any way?" - placeholder: "I use Cloudflare for ..." - - type: checkboxes - id: no-duplicate-issues - attributes: - label: "πŸ‘€ Have you spent some time to check if this issue has been raised before?" - description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" - options: - - label: "I checked and didn't find similar issue" - required: true - - type: checkboxes - id: read-code-of-conduct - attributes: - label: "🏒 Have you read the Code of Conduct?" - options: - - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" - required: true diff --git a/.github/ISSUE_TEMPLATE/documentation.yaml b/.github/ISSUE_TEMPLATE/documentation.yaml deleted file mode 100644 index 6cdefe0..0000000 --- a/.github/ISSUE_TEMPLATE/documentation.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: "πŸ“š Documentation" -description: "Report an issue related to documentation" -title: "πŸ“š Documentation: " -labels: [documentation] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to make our documentation betterπŸ™ - - type: textarea - id: issue-description - validations: - required: true - attributes: - label: "πŸ’­ Description" - description: "A clear and concise description of what the issue is." - placeholder: "Documentation should not ..." - - type: checkboxes - id: no-duplicate-issues - attributes: - label: "πŸ‘€ Have you spent some time to check if this issue has been raised before?" - description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" - options: - - label: "I checked and didn't find similar issue" - required: true - - type: checkboxes - id: read-code-of-conduct - attributes: - label: "🏒 Have you read the Code of Conduct?" - options: - - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" - required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml deleted file mode 100644 index d523a01..0000000 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: πŸš€ Feature" -description: "Submit a proposal for a new feature" -title: "πŸš€ Feature: " -labels: [feature] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out our bug report form πŸ™ - - type: textarea - id: feature-description - validations: - required: true - attributes: - label: "πŸ”– Feature description" - description: "A clear and concise description of what the feature is." - placeholder: "You should add ..." - - type: textarea - id: pitch - validations: - required: true - attributes: - label: "🎀 Pitch" - description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable." - placeholder: "In my use-case, ..." - - type: checkboxes - id: no-duplicate-issues - attributes: - label: "πŸ‘€ Have you spent some time to check if this issue has been raised before?" - description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" - options: - - label: "I checked and didn't find similar issue" - required: true - - type: checkboxes - id: read-code-of-conduct - attributes: - label: "🏒 Have you read the Code of Conduct?" - options: - - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" - required: true diff --git a/.github/workflows/autoclose.yml b/.github/workflows/autoclose.yml deleted file mode 100644 index 3e2b3cb..0000000 --- a/.github/workflows/autoclose.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Auto-close External Pull Requests - -on: - pull_request_target: - types: [opened, reopened] - -jobs: - auto_close: - uses: appwrite/.github/.github/workflows/autoclose.yml@main - secrets: - GH_AUTO_CLOSE_PR_TOKEN: ${{ secrets.GH_AUTO_CLOSE_PR_TOKEN }} From 3f6c2ddd448a89394790b6110404503c13bbd6d3 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 20 Apr 2026 14:07:52 +0530 Subject: [PATCH 4/5] chore: restore .github metadata Made-with: Cursor --- .github/ISSUE_TEMPLATE/bug.yaml | 82 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/documentation.yaml | 32 +++++++++ .github/ISSUE_TEMPLATE/feature.yaml | 40 +++++++++++ .github/workflows/autoclose.yml | 11 +++ 4 files changed, 165 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yaml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature.yaml create mode 100644 .github/workflows/autoclose.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000..175c606 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,82 @@ +name: "πŸ› Bug Report" +description: "Submit a bug report to help us improve" +title: "πŸ› Bug Report: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out our bug report form πŸ™ + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: "πŸ‘Ÿ Reproduction steps" + description: "How do you trigger this bug? Please walk us through it step by step." + placeholder: "When I ..." + - type: textarea + id: expected-behavior + validations: + required: true + attributes: + label: "πŸ‘ Expected behavior" + description: "What did you think would happen?" + placeholder: "It should ..." + - type: textarea + id: actual-behavior + validations: + required: true + attributes: + label: "πŸ‘Ž Actual Behavior" + description: "What did actually happen? Add screenshots, if applicable." + placeholder: "It actually ..." + - type: dropdown + id: appwrite-version + attributes: + label: "🎲 Appwrite version" + description: "What version of Appwrite are you running?" + options: + - Version 0.10.x + - Version 0.9.x + - Version 0.8.x + - Version 0.7.x + - Version 0.6.x + - Different version (specify in environment) + validations: + required: true + - type: dropdown + id: operating-system + attributes: + label: "πŸ’» Operating system" + description: "What OS is your server / device running on?" + options: + - Linux + - MacOS + - Windows + - Something else + validations: + required: true + - type: textarea + id: enviromnemt + validations: + required: false + attributes: + label: "🧱 Your Environment" + description: "Is your environment customized in any way?" + placeholder: "I use Cloudflare for ..." + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "πŸ‘€ Have you spent some time to check if this issue has been raised before?" + description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: "🏒 Have you read the Code of Conduct?" + options: + - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" + required: true diff --git a/.github/ISSUE_TEMPLATE/documentation.yaml b/.github/ISSUE_TEMPLATE/documentation.yaml new file mode 100644 index 0000000..6cdefe0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yaml @@ -0,0 +1,32 @@ +name: "πŸ“š Documentation" +description: "Report an issue related to documentation" +title: "πŸ“š Documentation: " +labels: [documentation] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to make our documentation betterπŸ™ + - type: textarea + id: issue-description + validations: + required: true + attributes: + label: "πŸ’­ Description" + description: "A clear and concise description of what the issue is." + placeholder: "Documentation should not ..." + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "πŸ‘€ Have you spent some time to check if this issue has been raised before?" + description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: "🏒 Have you read the Code of Conduct?" + options: + - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 0000000..d523a01 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,40 @@ +name: πŸš€ Feature" +description: "Submit a proposal for a new feature" +title: "πŸš€ Feature: " +labels: [feature] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out our bug report form πŸ™ + - type: textarea + id: feature-description + validations: + required: true + attributes: + label: "πŸ”– Feature description" + description: "A clear and concise description of what the feature is." + placeholder: "You should add ..." + - type: textarea + id: pitch + validations: + required: true + attributes: + label: "🎀 Pitch" + description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable." + placeholder: "In my use-case, ..." + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "πŸ‘€ Have you spent some time to check if this issue has been raised before?" + description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: "🏒 Have you read the Code of Conduct?" + options: + - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" + required: true diff --git a/.github/workflows/autoclose.yml b/.github/workflows/autoclose.yml new file mode 100644 index 0000000..3e2b3cb --- /dev/null +++ b/.github/workflows/autoclose.yml @@ -0,0 +1,11 @@ +name: Auto-close External Pull Requests + +on: + pull_request_target: + types: [opened, reopened] + +jobs: + auto_close: + uses: appwrite/.github/.github/workflows/autoclose.yml@main + secrets: + GH_AUTO_CLOSE_PR_TOKEN: ${{ secrets.GH_AUTO_CLOSE_PR_TOKEN }} From c936b87895c63a46881ad499c572c17917362746 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Apr 2026 09:16:51 +0000 Subject: [PATCH 5/5] chore: update Apple SDK to 17.0.0 --- CHANGELOG.md | 8 +- README.md | 6 +- Sources/Appwrite/Client.swift | 4 +- Sources/Appwrite/Models/RealtimeModels.swift | 44 +++- Sources/Appwrite/Services/Realtime.swift | 253 ++++++++++--------- Sources/AppwriteModels/Membership.swift | 9 + 6 files changed, 196 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2728ec5..7979a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Change Log -## 16.2.0 +## 17.0.0 -* Added: `Client.call()` converters now supported throwing parse transformations -* Fixed: Multipart upload conversion now propagated converter errors correctly -* Updated: README compatibility note now targets `latest` server version +* Breaking: Added `unsubscribe()`, `update()`, and `close()` for Realtime subscription lifecycle. +* Added: Added `userPhone` to the `Membership` model. +* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.2`. ## 16.1.0 diff --git a/README.md b/README.md index f370aff..24e0705 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ ![Swift Package Manager](https://img.shields.io/github/v/release/appwrite/sdk-for-apple.svg?color=green&style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-apple.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.1-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.2-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK is compatible with Appwrite server version latest. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-apple/releases).** +**This SDK is compatible with Appwrite server version 1.9.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-apple/releases).** Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Apple SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) @@ -31,7 +31,7 @@ Add the package to your `Package.swift` dependencies: ```swift dependencies: [ - .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "16.2.0"), + .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "17.0.0"), ], ``` diff --git a/Sources/Appwrite/Client.swift b/Sources/Appwrite/Client.swift index d798631..b06ef75 100644 --- a/Sources/Appwrite/Client.swift +++ b/Sources/Appwrite/Client.swift @@ -26,8 +26,8 @@ open class Client { "x-sdk-name": "Apple", "x-sdk-platform": "client", "x-sdk-language": "apple", - "x-sdk-version": "16.2.0", - "x-appwrite-response-format": "1.9.1" + "x-sdk-version": "17.0.0", + "x-appwrite-response-format": "1.9.2" ] internal var config: [String: String] = [:] diff --git a/Sources/Appwrite/Models/RealtimeModels.swift b/Sources/Appwrite/Models/RealtimeModels.swift index 5129b4c..e98cb92 100644 --- a/Sources/Appwrite/Models/RealtimeModels.swift +++ b/Sources/Appwrite/Models/RealtimeModels.swift @@ -1,26 +1,60 @@ import Foundation +public class RealtimeSubscriptionUpdate { + public let channels: [ChannelValue]? + public let queries: [String]? + + public init(channels: [ChannelValue]? = nil, queries: [String]? = nil) { + self.channels = channels + self.queries = queries + } +} + public class RealtimeSubscription { - private var close: () async throws -> Void + private var unsubscribeAction: () async throws -> Void + private var updateAction: (RealtimeSubscriptionUpdate) async throws -> Void + private var closeAction: () async throws -> Void + + init( + unsubscribe: @escaping () async throws -> Void, + update: @escaping (RealtimeSubscriptionUpdate) async throws -> Void, + close: @escaping () async throws -> Void + ) { + self.unsubscribeAction = unsubscribe + self.updateAction = update + self.closeAction = close + } + + /// Remove this subscription only. The WebSocket stays open so other subscriptions keep + /// receiving events; use `Realtime.disconnect()` to shut the connection down. + public func unsubscribe() async throws { + try await self.unsubscribeAction() + } - init(close: @escaping () async throws-> Void) { - self.close = close + /// Replace the channels and/or queries on this subscription without recreating it. + public func update(_ changes: RealtimeSubscriptionUpdate) async throws { + try await self.updateAction(changes) } + /// Alias of `unsubscribe()` that also tears the socket down when this was the last active + /// subscription. Prefer `unsubscribe()` plus `Realtime.disconnect()` for explicit control. public func close() async throws { - try await self.close() + try await self.closeAction() } } public class RealtimeCallback { - public let channels: Set + public var channels: Set + public var queries: [String] public let callback: (RealtimeResponseEvent) -> Void init( for channels: Set, + queries: [String], with callback: @escaping (RealtimeResponseEvent) -> Void ) { self.channels = channels + self.queries = queries self.callback = callback } } diff --git a/Sources/Appwrite/Services/Realtime.swift b/Sources/Appwrite/Services/Realtime.swift index 9f81800..7de3d10 100644 --- a/Sources/Appwrite/Services/Realtime.swift +++ b/Sources/Appwrite/Services/Realtime.swift @@ -5,6 +5,12 @@ import NIOHTTP1 open class Realtime : Service { + // Diagnostic messages go to stderr so they don't interleave with any stdout + // the host process may be asserting on (e.g. SDK integration tests). + private static func logDiagnostic(_ message: String) { + FileHandle.standardError.write(Data("\(message)\n".utf8)) + } + private let TYPE_ERROR = "error" private let TYPE_EVENT = "event" private let TYPE_PONG = "pong" @@ -13,21 +19,14 @@ open class Realtime : Service { private let HEARTBEAT_INTERVAL: UInt64 = 20_000_000_000 // 20 seconds in nanoseconds private var socketClient: WebSocketClient? = nil - // Slot-centric state: Map, queries: [String], callback: Function }> - private var activeSubscriptions = [Int: RealtimeCallback]() - private var activeSubscriptionQueries = [Int: [String]]() // Map slot -> queries array - // Map slot index -> subscriptionId (from backend) - private var slotToSubscriptionId = [Int: String]() - // Inverse map: subscriptionId -> slot index (for O(1) lookup) - private var subscriptionIdToSlot = [String: Int]() - private var pendingSubscribeSlots = [Int]() + private var activeSubscriptions = [String: RealtimeCallback]() + private var pendingSubscribes = [String: [String: Any]]() private var heartbeatTask: Task? = nil let connectSync = DispatchQueue(label: "ConnectSync") private var subCallDepth = 0 private var reconnectAttempts = 0 - private var subscriptionsCounter = 0 private var reconnect = true private var onErrorCallbacks: [((Swift.Error?, HTTPResponseStatus?) -> Void)] = [] @@ -58,7 +57,7 @@ open class Realtime : Service { } } catch { if !Task.isCancelled { - print("Heartbeat task failed: \(error.localizedDescription)") + Realtime.logDiagnostic("Heartbeat task failed: \(error.localizedDescription)") } } } @@ -94,30 +93,64 @@ open class Realtime : Service { try await socketClient?.connect() } - private func sendSubscribeMessage() { - guard let ws = socketClient, ws.isConnected else { + private func sendUnsubscribeMessage(_ subscriptionIds: [String]) { + let ids = subscriptionIds.filter { !$0.isEmpty } + guard !ids.isEmpty, let ws = socketClient, ws.isConnected else { return } + let payload: [String: Any] = [ + "type": "unsubscribe", + "data": ids.map { ["subscriptionId": $0] } + ] + if let data = try? JSONSerialization.data(withJSONObject: payload), + let text = String(data: data, encoding: .utf8) { + ws.send(text: text) + } + } - var rows = [[String: Any]]() - pendingSubscribeSlots.removeAll() - - for (slot, subscription) in activeSubscriptions { - let queries = activeSubscriptionQueries[slot] ?? [] - var row: [String: Any] = [ - "channels": Array(subscription.channels), - "queries": queries - ] - if let knownSubscriptionId = slotToSubscriptionId[slot] { - row["subscriptionId"] = knownSubscriptionId + private func generateUniqueSubscriptionId() throws -> String { + let attempts = activeSubscriptions.count + 1 + for _ in 0.. Void ) async throws -> RealtimeSubscription { - // Allocate a new slot index - subscriptionsCounter += 1 - let slot = subscriptionsCounter - - // Convert queries to array of strings - // queries is already [String], store as-is - let queryStrings = queries - - // Store slot-centric data: channels, queries, and callback belong to the slot - // queries is stored as [String] (array of query strings) - // No channel mutation occurs here - channels are derived from slots in createSocket() - activeSubscriptions[slot] = RealtimeCallback( + let subscriptionId = try generateUniqueSubscriptionId() + + activeSubscriptions[subscriptionId] = RealtimeCallback( for: channels, + queries: queries, with: callback ) - activeSubscriptionQueries[slot] = queryStrings + enqueuePendingSubscribe(subscriptionId: subscriptionId) connectSync.sync { subCallDepth += 1 } + defer { + connectSync.sync { + self.subCallDepth -= 1 + } + } try await Task.sleep(nanoseconds: UInt64(DEBOUNCE_NANOS)) if self.subCallDepth == 1 { if let ws = self.socketClient, ws.isConnected { - self.sendSubscribeMessage() + self.sendPendingSubscribes() } else { try await self.createSocket() } } - connectSync.sync { - self.subCallDepth -= 1 - } + return RealtimeSubscription( + unsubscribe: { [weak self] in + guard let self = self else { return } + guard self.activeSubscriptions[subscriptionId] != nil else { return } + self.activeSubscriptions[subscriptionId] = nil + self.pendingSubscribes[subscriptionId] = nil + self.sendUnsubscribeMessage([subscriptionId]) + }, + update: { [weak self] changes in + guard let self = self, let subscription = self.activeSubscriptions[subscriptionId] else { + return + } + if let nextChannels = changes.channels { + subscription.channels = Set(nextChannels.map { self.channelToString($0) }) + } + if let nextQueries = changes.queries { + subscription.queries = nextQueries + } - return RealtimeSubscription { - let subscriptionId = self.slotToSubscriptionId[slot] - self.activeSubscriptions[slot] = nil - self.activeSubscriptionQueries[slot] = nil - self.slotToSubscriptionId[slot] = nil - if let sid = subscriptionId { - self.subscriptionIdToSlot[sid] = nil - } - if self.activeSubscriptions.isEmpty { - self.reconnect = false - try await self.closeSocket() - return - } - if let ws = self.socketClient, ws.isConnected { - self.sendSubscribeMessage() - } else { - try await self.createSocket() + self.enqueuePendingSubscribe(subscriptionId: subscriptionId) + + self.connectSync.sync { + self.subCallDepth += 1 + } + defer { + self.connectSync.sync { + self.subCallDepth -= 1 + } + } + + try await Task.sleep(nanoseconds: UInt64(self.DEBOUNCE_NANOS)) + + if self.subCallDepth == 1 { + if let ws = self.socketClient, ws.isConnected { + self.sendPendingSubscribes() + } else { + try await self.createSocket() + } + } + }, + close: { [weak self] in + guard let self = self else { return } + if self.activeSubscriptions[subscriptionId] != nil { + self.activeSubscriptions[subscriptionId] = nil + self.pendingSubscribes[subscriptionId] = nil + self.sendUnsubscribeMessage([subscriptionId]) + } + if self.activeSubscriptions.isEmpty { + self.reconnect = false + try await self.closeSocket() + } } - } + ) } - - // cleanUp is no longer needed - slots are removed directly in subscribe().close() - // Channels are automatically rebuilt from remaining slots in createSocket() } extension Realtime: WebSocketClientDelegate { @@ -292,48 +349,20 @@ extension Realtime: WebSocketClientDelegate { } private func handleResponseConnected(from json: [String: Any]) { - guard let data = json["data"] as? [String: Any] else { + guard json["data"] is [String: Any] else { return } - - if let subscriptions = data["subscriptions"] as? [String: String] { - // Store subscription ID mappings from backend. - // Try direct slot first, then slot+1 for zero-based server maps. - for (slotStr, subscriptionId) in subscriptions { - if let slot = Int(slotStr) { - let targetSlot: Int - if activeSubscriptions[slot] != nil { - targetSlot = slot - } else if activeSubscriptions[slot + 1] != nil { - targetSlot = slot + 1 - } else { - targetSlot = slot - } - slotToSubscriptionId[targetSlot] = subscriptionId - subscriptionIdToSlot[subscriptionId] = targetSlot - } - } - } - sendSubscribeMessage() + for subscriptionId in activeSubscriptions.keys { + enqueuePendingSubscribe(subscriptionId: subscriptionId) + } + sendPendingSubscribes() } private func handleResponseAction(from json: [String: Any]) { - guard let data = json["data"] as? [String: Any], - data["to"] as? String == "subscribe", - let subscriptions = data["subscriptions"] as? [[String: Any]] else { - return - } - - for (index, item) in subscriptions.enumerated() { - guard index < pendingSubscribeSlots.count, - let subscriptionId = item["subscriptionId"] as? String else { - continue - } - let slot = pendingSubscribeSlots[index] - slotToSubscriptionId[slot] = subscriptionId - subscriptionIdToSlot[subscriptionId] = slot - } + // The SDK generates subscriptionIds client-side and sends them on every + // subscribe/unsubscribe, so subscribe/unsubscribe acks carry no state + // the SDK needs to reconcile. } public func onMessage(text: String) { @@ -375,7 +404,7 @@ extension Realtime: WebSocketClientDelegate { let timeout = getTimeout() - print("Realtime disconnected. Re-connecting in \(timeout / 1000) seconds.") + Realtime.logDiagnostic("Realtime disconnected. Re-connecting in \(timeout / 1000) seconds.") try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000)) @@ -386,7 +415,7 @@ extension Realtime: WebSocketClientDelegate { public func onError(error: Swift.Error?, status: HTTPResponseStatus?) { stopHeartbeat() - print(error?.localizedDescription ?? "Unknown error") + Realtime.logDiagnostic(error?.localizedDescription ?? "Unknown error") onErrorCallbacks.forEach { $0(error, status) } } @@ -408,19 +437,15 @@ extension Realtime: WebSocketClientDelegate { return } - // Iterate over all matching subscriptionIds and call callback for each for subscriptionId in subscriptions { - // O(1) lookup using subscriptionId - if let slot = subscriptionIdToSlot[subscriptionId] { - if let subscription = activeSubscriptions[slot] { - let response = RealtimeResponseEvent( - events: events, - channels: channels, - timestamp: data["timestamp"] as! String, - payload: payload - ) - subscription.callback(response) - } + if let subscription = activeSubscriptions[subscriptionId] { + let response = RealtimeResponseEvent( + events: events, + channels: channels, + timestamp: data["timestamp"] as? String ?? "", + payload: payload + ) + subscription.callback(response) } } } diff --git a/Sources/AppwriteModels/Membership.swift b/Sources/AppwriteModels/Membership.swift index 8ff61ee..277601c 100644 --- a/Sources/AppwriteModels/Membership.swift +++ b/Sources/AppwriteModels/Membership.swift @@ -11,6 +11,7 @@ open class Membership: Codable { case userId = "userId" case userName = "userName" case userEmail = "userEmail" + case userPhone = "userPhone" case teamId = "teamId" case teamName = "teamName" case invited = "invited" @@ -32,6 +33,8 @@ open class Membership: Codable { public let userName: String /// User email address. Hide this attribute by toggling membership privacy in the Console. public let userEmail: String + /// User phone number. Hide this attribute by toggling membership privacy in the Console. + public let userPhone: String /// Team ID. public let teamId: String /// Team name. @@ -54,6 +57,7 @@ open class Membership: Codable { userId: String, userName: String, userEmail: String, + userPhone: String, teamId: String, teamName: String, invited: String, @@ -68,6 +72,7 @@ open class Membership: Codable { self.userId = userId self.userName = userName self.userEmail = userEmail + self.userPhone = userPhone self.teamId = teamId self.teamName = teamName self.invited = invited @@ -86,6 +91,7 @@ open class Membership: Codable { self.userId = try container.decode(String.self, forKey: .userId) self.userName = try container.decode(String.self, forKey: .userName) self.userEmail = try container.decode(String.self, forKey: .userEmail) + self.userPhone = try container.decode(String.self, forKey: .userPhone) self.teamId = try container.decode(String.self, forKey: .teamId) self.teamName = try container.decode(String.self, forKey: .teamName) self.invited = try container.decode(String.self, forKey: .invited) @@ -104,6 +110,7 @@ open class Membership: Codable { try container.encode(userId, forKey: .userId) try container.encode(userName, forKey: .userName) try container.encode(userEmail, forKey: .userEmail) + try container.encode(userPhone, forKey: .userPhone) try container.encode(teamId, forKey: .teamId) try container.encode(teamName, forKey: .teamName) try container.encode(invited, forKey: .invited) @@ -121,6 +128,7 @@ open class Membership: Codable { "userId": userId as Any, "userName": userName as Any, "userEmail": userEmail as Any, + "userPhone": userPhone as Any, "teamId": teamId as Any, "teamName": teamName as Any, "invited": invited as Any, @@ -139,6 +147,7 @@ open class Membership: Codable { userId: map["userId"] as! String, userName: map["userName"] as! String, userEmail: map["userEmail"] as! String, + userPhone: map["userPhone"] as! String, teamId: map["teamId"] as! String, teamName: map["teamName"] as! String, invited: map["invited"] as! String,