From dff3c9bca565e56fc3a259baab2df027dfa076d6 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 27 Jun 2025 06:02:05 -0300 Subject: [PATCH 1/3] feat(realtime): add presence-enabled flag to join push - Add presenceEnabled property to RealtimeJoinConfig - Update CallbackManager to handle presence-enabled joins - Modify RealtimeChannelV2 to support presence-enabled configuration - Add tests for presence-enabled functionality - Update .gitignore for new test artifacts This change allows developers to explicitly enable/disable presence functionality when joining realtime channels, providing better control over presence behavior in realtime subscriptions. --- .gitignore | 2 ++ Sources/Realtime/CallbackManager.swift | 8 ++++++++ Sources/Realtime/RealtimeChannelV2.swift | 9 +++++++-- Sources/Realtime/RealtimeJoinConfig.swift | 1 + Tests/RealtimeTests/RealtimeTests.swift | 3 +++ 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d858f4161..93f84e0c1 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,5 @@ iOSInjectionProject/ Secrets.swift lcov.info temp_coverage + +.cursor \ No newline at end of file diff --git a/Sources/Realtime/CallbackManager.swift b/Sources/Realtime/CallbackManager.swift index 91a8e1990..e6c8d992d 100644 --- a/Sources/Realtime/CallbackManager.swift +++ b/Sources/Realtime/CallbackManager.swift @@ -204,4 +204,12 @@ enum RealtimeCallback { case let .system(callback): callback.id } } + + var isPresence: Bool { + if case .presence = self { + return true + } else { + return false + } + } } diff --git a/Sources/Realtime/RealtimeChannelV2.swift b/Sources/Realtime/RealtimeChannelV2.swift index d74478821..4ec0d42c5 100644 --- a/Sources/Realtime/RealtimeChannelV2.swift +++ b/Sources/Realtime/RealtimeChannelV2.swift @@ -35,7 +35,9 @@ public final class RealtimeChannelV2: Sendable { private var mutableState = MutableState() let topic: String - let config: RealtimeChannelConfig + + @MainActor var config: RealtimeChannelConfig + let logger: (any SupabaseLogger)? let socket: RealtimeClientV2 @@ -97,6 +99,8 @@ public final class RealtimeChannelV2: Sendable { status = .subscribing logger?.debug("Subscribing to channel \(topic)") + config.presence.enabled = callbackManager.callbacks.contains(where: { $0.isPresence }) + let joinConfig = RealtimeJoinConfig( broadcast: config.broadcast, presence: config.presence, @@ -168,6 +172,7 @@ public final class RealtimeChannelV2: Sendable { /// - Parameters: /// - event: Broadcast message event. /// - message: Message payload. + @MainActor public func broadcast(event: String, message: JSONObject) async { if status != .subscribed { struct Message: Encodable { @@ -374,7 +379,7 @@ public final class RealtimeChannelV2: Sendable { status = .unsubscribed case .error: - logger?.debug( + logger?.error( "Received an error in channel \(message.topic). That could be as a result of an invalid access token" ) diff --git a/Sources/Realtime/RealtimeJoinConfig.swift b/Sources/Realtime/RealtimeJoinConfig.swift index a617757f2..3c5df1f38 100644 --- a/Sources/Realtime/RealtimeJoinConfig.swift +++ b/Sources/Realtime/RealtimeJoinConfig.swift @@ -50,6 +50,7 @@ public struct BroadcastJoinConfig: Codable, Hashable, Sendable { public struct PresenceJoinConfig: Codable, Hashable, Sendable { /// Track presence payload across clients. public var key: String = "" + var enabled: Bool = false } public enum PostgresChangeEvent: String, Codable, Sendable { diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index 54d26be0f..59cb9ff5c 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -169,6 +169,7 @@ final class RealtimeTests: XCTestCase { } ], "presence" : { + "enabled" : false, "key" : "" }, "private" : false @@ -241,6 +242,7 @@ final class RealtimeTests: XCTestCase { ], "presence" : { + "enabled" : false, "key" : "" }, "private" : false @@ -264,6 +266,7 @@ final class RealtimeTests: XCTestCase { ], "presence" : { + "enabled" : false, "key" : "" }, "private" : false From c4f1f1fcfb3a098b2c3dccdccfe6e5e876d9ffcb Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Fri, 27 Jun 2025 07:24:53 -0300 Subject: [PATCH 2/3] test(realtime): add test for presence enabled flag during subscribe - Add comprehensive test that verifies presence.enabled is set to true when presence callback exists - Test uses FakeWebSocket to simulate real subscription flow - Verifies that presence enabled flag is correctly set in phx_join payload - Ensures proper cleanup of subscriptions and connections This test validates the core functionality where presence enabled is automatically set based on presence callback existence. --- .../RealtimeTests/RealtimeChannelTests.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Tests/RealtimeTests/RealtimeChannelTests.swift b/Tests/RealtimeTests/RealtimeChannelTests.swift index c213d2d65..8589519d5 100644 --- a/Tests/RealtimeTests/RealtimeChannelTests.swift +++ b/Tests/RealtimeTests/RealtimeChannelTests.swift @@ -6,6 +6,7 @@ // import InlineSnapshotTesting +import TestHelpers import XCTest import XCTestDynamicOverlay @@ -128,4 +129,67 @@ final class RealtimeChannelTests: XCTestCase { """ } } + + @MainActor + func testPresenceEnabledDuringSubscribe() async { + // Create fake WebSocket for testing + let (client, server) = FakeWebSocket.fakes() + + let socket = RealtimeClientV2( + url: URL(string: "https://localhost:54321/realtime/v1")!, + options: RealtimeClientOptions( + headers: ["apikey": "test-key"], + accessToken: { "test-token" } + ), + wsTransport: { _, _ in client }, + http: HTTPClientMock() + ) + + // Create a channel without presence callback initially + let channel = socket.channel("test-topic") + + // Initially presence should be disabled + XCTAssertFalse(channel.config.presence.enabled) + + // Connect the socket + await socket.connect() + + // Add a presence callback before subscribing + let presenceSubscription = channel.onPresenceChange { _ in } + + // Verify that presence callback exists + XCTAssertTrue(channel.callbackManager.callbacks.contains(where: { $0.isPresence })) + + // Start subscription process + Task { + await channel.subscribe() + } + + // Wait for the join message to be sent + await Task.megaYield() + + // Check the sent events to verify presence enabled is set correctly + let joinEvents = server.receivedEvents.compactMap { $0.realtimeMessage }.filter { + $0.event == "phx_join" + } + + // Should have at least one join event + XCTAssertGreaterThan(joinEvents.count, 0) + + // Check that the presence enabled flag is set to true in the join payload + if let joinEvent = joinEvents.first, + let config = joinEvent.payload["config"]?.objectValue, + let presence = config["presence"]?.objectValue, + let enabled = presence["enabled"]?.boolValue + { + XCTAssertTrue(enabled, "Presence should be enabled when presence callback exists") + } else { + XCTFail("Could not find presence enabled flag in join payload") + } + + // Clean up + presenceSubscription.cancel() + await channel.unsubscribe() + socket.disconnect() + } } From 5f6faae76383ee0e1c42968d11bfe29e2fd2a891 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 30 Jun 2025 08:25:00 -0300 Subject: [PATCH 3/3] fix(realtime): resubscribe channel when presence callback added to subscribed channel When a presence callback is added to an already subscribed channel, the channel now automatically resubscribes to ensure the presence functionality works correctly. This fixes an issue where presence callbacks added after subscription would not receive presence updates. - Add resubscription logic in onPresenceChange method - Log debug message when resubscribing - Maintain existing callback registration behavior --- Sources/Realtime/RealtimeChannelV2.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/Realtime/RealtimeChannelV2.swift b/Sources/Realtime/RealtimeChannelV2.swift index 4ec0d42c5..9b6efa14c 100644 --- a/Sources/Realtime/RealtimeChannelV2.swift +++ b/Sources/Realtime/RealtimeChannelV2.swift @@ -401,7 +401,18 @@ public final class RealtimeChannelV2: Sendable { public func onPresenceChange( _ callback: @escaping @Sendable (any PresenceAction) -> Void ) -> RealtimeSubscription { + if status == .subscribed { + logger?.debug( + "Resubscribe to \(self.topic) due to change in presence callback on joined channel." + ) + Task { + await unsubscribe() + await subscribe() + } + } + let id = callbackManager.addPresenceCallback(callback: callback) + return RealtimeSubscription { [weak callbackManager, logger] in logger?.debug("Removing presence callback with id: \(id)") callbackManager?.removeCallback(id: id)