Skip to content

feat(realtime): add presence-enabled flag to join push #736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,5 @@ iOSInjectionProject/
Secrets.swift
lcov.info
temp_coverage

.cursor
8 changes: 8 additions & 0 deletions Sources/Realtime/CallbackManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,12 @@ enum RealtimeCallback {
case let .system(callback): callback.id
}
}

var isPresence: Bool {
if case .presence = self {
return true
} else {
return false
}
}
}
20 changes: 18 additions & 2 deletions Sources/Realtime/RealtimeChannelV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
}

@MainActor
private var mutableState = MutableState()

Check warning on line 35 in Sources/Realtime/RealtimeChannelV2.swift

View workflow job for this annotation

GitHub Actions / xcodebuild (15) (MACOS, 15.2)

stored property 'mutableState' of 'Sendable'-conforming class 'RealtimeChannelV2' is mutable

let topic: String
let config: RealtimeChannelConfig

@MainActor var config: RealtimeChannelConfig

let logger: (any SupabaseLogger)?
let socket: RealtimeClientV2

Expand Down Expand Up @@ -97,6 +99,8 @@
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,
Expand Down Expand Up @@ -168,6 +172,7 @@
/// - Parameters:
/// - event: Broadcast message event.
/// - message: Message payload.
@MainActor
public func broadcast(event: String, message: JSONObject) async {
if status != .subscribed {
struct Message: Encodable {
Expand Down Expand Up @@ -374,7 +379,7 @@
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"
)

Expand All @@ -396,7 +401,18 @@
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)
Expand Down
1 change: 1 addition & 0 deletions Sources/Realtime/RealtimeJoinConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
64 changes: 64 additions & 0 deletions Tests/RealtimeTests/RealtimeChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import InlineSnapshotTesting
import TestHelpers
import XCTest
import XCTestDynamicOverlay

Expand Down Expand Up @@ -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()
}
}
3 changes: 3 additions & 0 deletions Tests/RealtimeTests/RealtimeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ final class RealtimeTests: XCTestCase {
}
],
"presence" : {
"enabled" : false,
"key" : ""
},
"private" : false
Expand Down Expand Up @@ -241,6 +242,7 @@ final class RealtimeTests: XCTestCase {

],
"presence" : {
"enabled" : false,
"key" : ""
},
"private" : false
Expand All @@ -264,6 +266,7 @@ final class RealtimeTests: XCTestCase {

],
"presence" : {
"enabled" : false,
"key" : ""
},
"private" : false
Expand Down
Loading