Skip to content

Commit 7de5b8b

Browse files
authored
feat(realtime): add presence-enabled flag to join push (#736)
* 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. * 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. * 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
1 parent 09f047b commit 7de5b8b

File tree

6 files changed

+96
-2
lines changed

6 files changed

+96
-2
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,5 @@ iOSInjectionProject/
101101
Secrets.swift
102102
lcov.info
103103
temp_coverage
104+
105+
.cursor

Sources/Realtime/CallbackManager.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,12 @@ enum RealtimeCallback {
204204
case let .system(callback): callback.id
205205
}
206206
}
207+
208+
var isPresence: Bool {
209+
if case .presence = self {
210+
return true
211+
} else {
212+
return false
213+
}
214+
}
207215
}

Sources/Realtime/RealtimeChannelV2.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ public final class RealtimeChannelV2: Sendable {
3535
private var mutableState = MutableState()
3636

3737
let topic: String
38-
let config: RealtimeChannelConfig
38+
39+
@MainActor var config: RealtimeChannelConfig
40+
3941
let logger: (any SupabaseLogger)?
4042
let socket: RealtimeClientV2
4143

@@ -97,6 +99,8 @@ public final class RealtimeChannelV2: Sendable {
9799
status = .subscribing
98100
logger?.debug("Subscribing to channel \(topic)")
99101

102+
config.presence.enabled = callbackManager.callbacks.contains(where: { $0.isPresence })
103+
100104
let joinConfig = RealtimeJoinConfig(
101105
broadcast: config.broadcast,
102106
presence: config.presence,
@@ -168,6 +172,7 @@ public final class RealtimeChannelV2: Sendable {
168172
/// - Parameters:
169173
/// - event: Broadcast message event.
170174
/// - message: Message payload.
175+
@MainActor
171176
public func broadcast(event: String, message: JSONObject) async {
172177
if status != .subscribed {
173178
struct Message: Encodable {
@@ -374,7 +379,7 @@ public final class RealtimeChannelV2: Sendable {
374379
status = .unsubscribed
375380

376381
case .error:
377-
logger?.debug(
382+
logger?.error(
378383
"Received an error in channel \(message.topic). That could be as a result of an invalid access token"
379384
)
380385

@@ -396,7 +401,18 @@ public final class RealtimeChannelV2: Sendable {
396401
public func onPresenceChange(
397402
_ callback: @escaping @Sendable (any PresenceAction) -> Void
398403
) -> RealtimeSubscription {
404+
if status == .subscribed {
405+
logger?.debug(
406+
"Resubscribe to \(self.topic) due to change in presence callback on joined channel."
407+
)
408+
Task {
409+
await unsubscribe()
410+
await subscribe()
411+
}
412+
}
413+
399414
let id = callbackManager.addPresenceCallback(callback: callback)
415+
400416
return RealtimeSubscription { [weak callbackManager, logger] in
401417
logger?.debug("Removing presence callback with id: \(id)")
402418
callbackManager?.removeCallback(id: id)

Sources/Realtime/RealtimeJoinConfig.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public struct BroadcastJoinConfig: Codable, Hashable, Sendable {
5050
public struct PresenceJoinConfig: Codable, Hashable, Sendable {
5151
/// Track presence payload across clients.
5252
public var key: String = ""
53+
var enabled: Bool = false
5354
}
5455

5556
public enum PostgresChangeEvent: String, Codable, Sendable {

Tests/RealtimeTests/RealtimeChannelTests.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import InlineSnapshotTesting
9+
import TestHelpers
910
import XCTest
1011
import XCTestDynamicOverlay
1112

@@ -128,4 +129,67 @@ final class RealtimeChannelTests: XCTestCase {
128129
"""
129130
}
130131
}
132+
133+
@MainActor
134+
func testPresenceEnabledDuringSubscribe() async {
135+
// Create fake WebSocket for testing
136+
let (client, server) = FakeWebSocket.fakes()
137+
138+
let socket = RealtimeClientV2(
139+
url: URL(string: "https://localhost:54321/realtime/v1")!,
140+
options: RealtimeClientOptions(
141+
headers: ["apikey": "test-key"],
142+
accessToken: { "test-token" }
143+
),
144+
wsTransport: { _, _ in client },
145+
http: HTTPClientMock()
146+
)
147+
148+
// Create a channel without presence callback initially
149+
let channel = socket.channel("test-topic")
150+
151+
// Initially presence should be disabled
152+
XCTAssertFalse(channel.config.presence.enabled)
153+
154+
// Connect the socket
155+
await socket.connect()
156+
157+
// Add a presence callback before subscribing
158+
let presenceSubscription = channel.onPresenceChange { _ in }
159+
160+
// Verify that presence callback exists
161+
XCTAssertTrue(channel.callbackManager.callbacks.contains(where: { $0.isPresence }))
162+
163+
// Start subscription process
164+
Task {
165+
await channel.subscribe()
166+
}
167+
168+
// Wait for the join message to be sent
169+
await Task.megaYield()
170+
171+
// Check the sent events to verify presence enabled is set correctly
172+
let joinEvents = server.receivedEvents.compactMap { $0.realtimeMessage }.filter {
173+
$0.event == "phx_join"
174+
}
175+
176+
// Should have at least one join event
177+
XCTAssertGreaterThan(joinEvents.count, 0)
178+
179+
// Check that the presence enabled flag is set to true in the join payload
180+
if let joinEvent = joinEvents.first,
181+
let config = joinEvent.payload["config"]?.objectValue,
182+
let presence = config["presence"]?.objectValue,
183+
let enabled = presence["enabled"]?.boolValue
184+
{
185+
XCTAssertTrue(enabled, "Presence should be enabled when presence callback exists")
186+
} else {
187+
XCTFail("Could not find presence enabled flag in join payload")
188+
}
189+
190+
// Clean up
191+
presenceSubscription.cancel()
192+
await channel.unsubscribe()
193+
socket.disconnect()
194+
}
131195
}

Tests/RealtimeTests/RealtimeTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ final class RealtimeTests: XCTestCase {
169169
}
170170
],
171171
"presence" : {
172+
"enabled" : false,
172173
"key" : ""
173174
},
174175
"private" : false
@@ -241,6 +242,7 @@ final class RealtimeTests: XCTestCase {
241242
242243
],
243244
"presence" : {
245+
"enabled" : false,
244246
"key" : ""
245247
},
246248
"private" : false
@@ -264,6 +266,7 @@ final class RealtimeTests: XCTestCase {
264266
265267
],
266268
"presence" : {
269+
"enabled" : false,
267270
"key" : ""
268271
},
269272
"private" : false

0 commit comments

Comments
 (0)