Skip to content
Merged
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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## 13.3.0

* Add `onOpen`, `onClose` and `onError` callbacks to `Realtime` service

## 13.2.2

* Fix issue: Missing AppwriteEnums dependency causing build failure
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Add the package to your `Package.swift` dependencies:

```swift
dependencies: [
.package(url: "[email protected]:appwrite/sdk-for-apple.git", from: "13.2.2"),
.package(url: "[email protected]:appwrite/sdk-for-apple.git", from: "13.3.0"),
],
```

Expand Down
2 changes: 1 addition & 1 deletion Sources/Appwrite/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ open class Client {
"x-sdk-name": "Apple",
"x-sdk-platform": "client",
"x-sdk-language": "apple",
"x-sdk-version": "13.2.2",
"x-sdk-version": "13.3.0",
"x-appwrite-response-format": "1.8.0"
]

Expand Down
21 changes: 21 additions & 0 deletions Sources/Appwrite/Services/Realtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ open class Realtime : Service {
private var reconnectAttempts = 0
private var subscriptionsCounter = 0
private var reconnect = true

private var onErrorCallbacks: [((Swift.Error?, HTTPResponseStatus?) -> Void)] = []
private var onCloseCallbacks: [(() -> Void)] = []
private var onOpenCallbacks: [(() -> Void)] = []

public func onError(_ callback: @escaping (Swift.Error?, HTTPResponseStatus?) -> Void) {
self.onErrorCallbacks.append(callback)
}

public func onClose(_ callback: @escaping () -> Void) {
self.onCloseCallbacks.append(callback)
}

public func onOpen(_ callback: @escaping () -> Void) {
self.onOpenCallbacks.append(callback)
}
Comment on lines +26 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider thread safety for callback storage and registration.

The callback arrays are accessed without synchronization. If onOpen, onClose, or onError registration methods are called concurrently with callback invocations (lines 210, 231, 253), or if multiple threads register callbacks simultaneously, this could lead to race conditions.

Consider using the existing connectSync queue or introducing dedicated synchronization for callback array access.

Apply this pattern to protect array mutations:

 public func onError(_ callback: @escaping (Swift.Error?, HTTPResponseStatus?) -> Void) {
-    self.onErrorCallbacks.append(callback)
+    connectSync.sync {
+        self.onErrorCallbacks.append(callback)
+    }
 }

Apply similar synchronization to onClose and onOpen methods, and protect the forEach invocations with the same queue.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Sources/Appwrite/Services/Realtime.swift around lines 26 to 40, the callback
arrays (onErrorCallbacks, onCloseCallbacks, onOpenCallbacks) are mutated and
read without synchronization which can cause race conditions; protect all
mutations (append) and iterations (forEach invocations at lines ~210, 231, 253)
by using the existing connectSync dispatch queue (or a dedicated serial
DispatchQueue) to serialize access: wrap appends in connectSync.async or sync as
appropriate, and wrap callback invocations in connectSync.sync/async while
copying the array to a local constant before iteration to avoid holding the
queue during callbacks. Ensure the same queue is used for registration and
invocation for each callback array to guarantee thread safety.


private func startHeartbeat() {
stopHeartbeat()
Expand Down Expand Up @@ -191,6 +207,7 @@ extension Realtime: WebSocketClientDelegate {

public func onOpen(channel: Channel) {
self.reconnectAttempts = 0
onOpenCallbacks.forEach { $0() }
startHeartbeat()
}

Expand All @@ -210,6 +227,8 @@ extension Realtime: WebSocketClientDelegate {

public func onClose(channel: Channel, data: Data) async throws {
stopHeartbeat()

onCloseCallbacks.forEach { $0() }

if (!reconnect) {
reconnect = true
Expand All @@ -230,6 +249,8 @@ extension Realtime: WebSocketClientDelegate {
public func onError(error: Swift.Error?, status: HTTPResponseStatus?) {
stopHeartbeat()
print(error?.localizedDescription ?? "Unknown error")

onErrorCallbacks.forEach { $0(error, status) }
}

func handleResponseError(from json: [String: Any]) throws {
Expand Down