Skip to content

Add subscription handling to Realtime services across multiple platforms#1427

Merged
ArnabChatterjee20k merged 8 commits intomasterfrom
realtime-queries-message
Apr 18, 2026
Merged

Add subscription handling to Realtime services across multiple platforms#1427
ArnabChatterjee20k merged 8 commits intomasterfrom
realtime-queries-message

Conversation

@ArnabChatterjee20k
Copy link
Copy Markdown
Member

@ArnabChatterjee20k ArnabChatterjee20k commented Apr 7, 2026

  • Investigate CI failures for Android and Flutter builds
  • Fix Android: Add missing import toJson to Realtime.kt.twig template
  • Revert staging URL in test files (flutter/tests.dart, android/Tests.kt, apple/Tests.swift, web/index.html)
  • Regenerate Android examples after template fix
  • Validate fixes with parallel validation

- Introduced TYPE_RESPONSE constant to handle subscription responses in Realtime services for Android, iOS, Flutter, React Native, and Web.
- Implemented sendSubscribeMessage method to manage subscription messages and maintain pending subscription slots.
- Updated socket connection logic to reuse existing connections and send updated subscription messages when necessary.
- Refactored query parameter construction to streamline the subscription process, removing redundant channel and query handling from the URL.
- Enhanced subscription ID mapping to accommodate zero-based server mappings, ensuring accurate subscription management across platforms.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 7, 2026

Greptile Summary

This PR migrates the Realtime subscription mechanism across Android, Apple/Swift, Flutter, Web, and React Native templates from URL query-parameter channel encoding to a WebSocket-level subscribe JSON message protocol. It introduces slot-based subscription tracking (slotToSubscriptionId / subscriptionIdToSlot) for O(1) event dispatch and updates event delivery to use server-assigned subscription IDs.

Notable improvements:

  • Android: Replaces the flat pendingSubscribeSlots list with pendingSubscribeSlotsQueue (ArrayDeque<List<Int>>), correctly handling concurrent in-flight subscribe messages and eliminating the slot-corruption race condition.
  • Android: Adds the missing import toJson and the TYPE_RESPONSE constant.
  • Web: The unsubscribe closure now cleans up this.realtime.channels by removing channels no longer referenced by any active subscription, fixing a stale-set leak.
  • All platforms: Removes the slot - 1 fallback that was previously assigning a new subscription the server-side ID of the prior slot.

The key outstanding item is that all four modified test files (flutter/tests.dart, apple/Tests.swift, android/Tests.kt, web/index.html) still use a hardcoded staging endpoint (wss://fra.stage.cloud.appwrite.io/v1). This is explicitly listed as an unchecked TODO in the PR description and will break CI tests. The PR checklist also notes that Android examples need to be regenerated after the template fix.

Confidence Score: 3/5

Not safe to merge — all four test files point at staging infrastructure and the PR's own checklist has uncompleted items.

The subscription-logic improvements are solid and address previously identified bugs (slot-1 fallback, missing import, stale channels set, Android queue pattern). However, all four modified test files still use the hardcoded staging endpoint explicitly listed as an unchecked TODO in the PR description, which will break CI. Android examples also need regeneration per the checklist.

tests/languages/flutter/tests.dart, tests/languages/apple/Tests.swift, tests/languages/android/Tests.kt, tests/languages/web/index.html — all have staging URLs that must be reverted before merging.

Important Files Changed

Filename Overview
templates/android/library/src/main/java/io/package/services/Realtime.kt.twig Adds queue-based pending slot tracking, missing toJson import, and TYPE_RESPONSE constant; previous synchronisation concerns addressed
templates/apple/Sources/Services/Realtime.swift.twig Removes slot-1 fallback and implements subscribe-message pattern; single pendingSubscribeSlots array without thread-safety (previously flagged) remains
templates/flutter/lib/src/realtime_mixin.dart.twig Clean Dart implementation with proper WebSocket open-state guard in _sendSubscribeMessage and slot-based O(1) event dispatch
templates/react-native/src/client.ts.twig Good subscribe-message pattern; pendingSubscribeSlots is a flat array (not a queue) that can be overwritten before server responds on rapid unsubscribe/re-subscribe
templates/web/src/client.ts.twig Channels set now properly cleaned up on unsubscribe; legacy fallback dispatch path retained for backward compatibility
templates/web/src/services/realtime.ts.twig Clean standalone Realtime class with correct WebSocket readyState guards and O(1) slot-based event dispatch
tests/languages/android/Tests.kt Uses staging endpoint wss://fra.stage.cloud.appwrite.io/v1 — an explicitly unchecked TODO in the PR
tests/languages/apple/Tests.swift Uses staging endpoint wss://fra.stage.cloud.appwrite.io/v1 — an explicitly unchecked TODO in the PR
tests/languages/flutter/tests.dart Uses staging endpoint wss://fra.stage.cloud.appwrite.io/v1 — an explicitly unchecked TODO in the PR
tests/languages/web/index.html Uses staging endpoint wss://fra.stage.cloud.appwrite.io/v1 — an explicitly unchecked TODO in the PR

Reviews (6): Last reviewed commit: "updated" | Re-trigger Greptile

Comment thread templates/android/library/src/main/java/io/package/services/Realtime.kt.twig Outdated
Comment thread templates/flutter/lib/src/realtime_mixin.dart.twig Outdated
Comment thread templates/web/src/client.ts.twig
Comment thread templates/apple/Sources/Services/Realtime.swift.twig Outdated
Comment thread templates/flutter/lib/src/realtime_mixin.dart.twig Outdated
Comment thread templates/web/src/client.ts.twig Outdated
Comment thread templates/react-native/src/client.ts.twig Outdated
Comment thread templates/web/src/services/realtime.ts.twig Outdated
…size and handling it in the backend side for the empty queries
…le platforms

- Updated the logic for retrieving known subscription IDs to remove fallback to the previous slot, ensuring only the current slot is considered.
- This change was applied consistently across Android, iOS, Flutter, React Native, and Web implementations.
@ArnabChatterjee20k
Copy link
Copy Markdown
Member Author

@copilot can you check why build is failing in the android and flutter

Comment on lines +97 to +130
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)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 sendSubscribeMessage() accesses shared state without synchronization

Unlike the Android template (which guards slot/subscription mutations with subscriptionLock), the Swift sendSubscribeMessage() reads and mutates pendingSubscribeSlots and iterates activeSubscriptions with no synchronization:

pendingSubscribeSlots.removeAll()   // no lock
for (slot, subscription) in activeSubscriptions { ... }  // no lock

sendSubscribeMessage() is reachable concurrently from:

  • handleResponseConnected() — fires on whatever thread the underlying WebSocketClient uses for callbacks
  • subscribe() — runs in an async Task context

A concurrent call from the WebSocket callback thread while subscribe() iterates activeSubscriptions creates a data race on Swift's non-thread-safe Dictionary and Array types (undefined behavior). Even without a crash, if two calls interleave, pendingSubscribeSlots from the first call can be overwritten before the server's response arrives, causing handleResponseAction to assign subscription IDs to the wrong slots.

The existing connectSync serial DispatchQueue is the right primitive. Running sendSubscribeMessage() synchronously on it serializes all access to the shared state:

private func sendSubscribeMessage() {
    connectSync.sync {
        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 sid = slotToSubscriptionId[slot] { row["subscriptionId"] = sid }
            rows.append(row)
            pendingSubscribeSlots.append(slot)
        }
        guard !rows.isEmpty,
              let data = try? JSONSerialization.data(withJSONObject: ["type": "subscribe", "data": rows]),
              let text = String(data: data, encoding: .utf8) else { return }
        ws.send(text: text)
    }
}

Copilot stopped work on behalf of ArnabChatterjee20k due to an error April 7, 2026 12:56
@ArnabChatterjee20k ArnabChatterjee20k merged commit eab15b1 into master Apr 18, 2026
57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants