Skip to content
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
6 changes: 6 additions & 0 deletions Sources/Realtime/RealtimeMessageV2.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import Foundation

/// A message sent over the Realtime WebSocket connection.
///
/// Both `joinRef` and `ref` are optional because certain messages like heartbeats
/// don't require a join reference as they don't refer to a specific channel.
public struct RealtimeMessageV2: Hashable, Codable, Sendable {
/// Optional join reference. Nil for messages like heartbeats that don't belong to a specific channel.
public let joinRef: String?
/// Optional message reference. Can be nil for certain message types.
public let ref: String?
public let topic: String
public let event: String
Expand Down
76 changes: 76 additions & 0 deletions Tests/RealtimeTests/RealtimeMessageV2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,80 @@ final class RealtimeMessageV2Tests: XCTestCase {
joinRef: nil, ref: nil, topic: "topic", event: "unknown_event", payload: payloadWithNoStatus)
XCTAssertNil(unknownEventMessage._eventType)
}

func testMessageWithNilRefs() throws {
let message = RealtimeMessageV2(
joinRef: nil,
ref: nil,
topic: "phoenix",
event: "heartbeat",
payload: [:]
)
// Verify JSON encoding works
let encoded = try JSONEncoder().encode(message)
let decoded = try JSONDecoder().decode(RealtimeMessageV2.self, from: encoded)
XCTAssertNil(decoded.joinRef)
XCTAssertNil(decoded.ref)
XCTAssertEqual(decoded.topic, "phoenix")
XCTAssertEqual(decoded.event, "heartbeat")
}

func testHeartbeatMessageWithNilJoinRef() throws {
let message = RealtimeMessageV2(
joinRef: nil, // Heartbeats don't have joinRef
ref: "123",
topic: "phoenix",
event: "heartbeat",
payload: [:]
)
let encoded = try JSONEncoder().encode(message)
let decoded = try JSONDecoder().decode(RealtimeMessageV2.self, from: encoded)
XCTAssertNil(decoded.joinRef)
XCTAssertEqual(decoded.ref, "123")
}

func testMessageJSONEncodingWithNilRefs() throws {
let message = RealtimeMessageV2(
joinRef: nil,
ref: nil,
topic: "test",
event: "custom",
payload: ["key": "value"]
)
let encoded = try JSONEncoder().encode(message)
let jsonString = String(data: encoded, encoding: .utf8)!
// Verify nil values are encoded as null in JSON
XCTAssertTrue(jsonString.contains("\"join_ref\":null") || !jsonString.contains("join_ref"))
XCTAssertTrue(jsonString.contains("\"ref\":null") || !jsonString.contains("ref"))
}

func testMessageWithBothRefAndJoinRef() throws {
let message = RealtimeMessageV2(
joinRef: "join-456",
ref: "ref-789",
topic: "room:lobby",
event: "join",
payload: ["user_id": "123"]
)
let encoded = try JSONEncoder().encode(message)
let decoded = try JSONDecoder().decode(RealtimeMessageV2.self, from: encoded)
XCTAssertEqual(decoded.joinRef, "join-456")
XCTAssertEqual(decoded.ref, "ref-789")
XCTAssertEqual(decoded.topic, "room:lobby")
}

func testMessageWithRefButNilJoinRef() throws {
let message = RealtimeMessageV2(
joinRef: nil,
ref: "ref-999",
topic: "room:lobby",
event: "leave",
payload: [:]
)
let encoded = try JSONEncoder().encode(message)
let decoded = try JSONDecoder().decode(RealtimeMessageV2.self, from: encoded)
XCTAssertNil(decoded.joinRef)
XCTAssertEqual(decoded.ref, "ref-999")
XCTAssertEqual(decoded.topic, "room:lobby")
}
}
Loading