Skip to content

Commit

Permalink
Add migration guide
Browse files Browse the repository at this point in the history
  • Loading branch information
grdsdev committed Jan 22, 2024
1 parent fcf3ed8 commit 2d523f0
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 34 deletions.
6 changes: 6 additions & 0 deletions Sources/Realtime/Presence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ import Foundation
/// }
///
/// presence.onSync { renderUsers(presence.list()) }
@available(
*,
deprecated,
renamed: "PresenceV2",
message: "Presence class is deprecated in favor of PresenceV2."
)
public final class Presence {
// ----------------------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions Sources/Realtime/V2/CallbackManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ final class CallbackManager: @unchecked Sendable {
}

func triggerPresenceDiffs(
joins: [String: _Presence],
leaves: [String: _Presence],
joins: [String: PresenceV2],
leaves: [String: PresenceV2],
rawMessage: RealtimeMessageV2
) {
let presenceCallbacks = mutableState.callbacks.compactMap {
Expand Down
11 changes: 7 additions & 4 deletions Sources/Realtime/V2/PostgresAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,16 @@ public enum AnyAction: PostgresAction, HasRawMessage {
}

extension HasRecord {
public func decodeRecord<T: Decodable>(decoder: JSONDecoder) throws -> T {
try record.decode(T.self, decoder: decoder)
public func decodeRecord<T: Decodable>(as _: T.Type = T.self, decoder: JSONDecoder) throws -> T {
try record.decode(as: T.self, decoder: decoder)
}
}

extension HasOldRecord {
public func decodeOldRecord<T: Decodable>(decoder: JSONDecoder) throws -> T {
try oldRecord.decode(T.self, decoder: decoder)
public func decodeOldRecord<T: Decodable>(
as _: T.Type = T.self,
decoder: JSONDecoder
) throws -> T {
try oldRecord.decode(as: T.self, decoder: decoder)
}
}
33 changes: 18 additions & 15 deletions Sources/Realtime/V2/PresenceAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
import Foundation
@_spi(Internal) import _Helpers

public struct _Presence: Hashable, Sendable {
public struct PresenceV2: Hashable, Sendable {
public let ref: String
public let state: JSONObject
}

extension _Presence: Codable {
extension PresenceV2: Codable {
struct _StringCodingKey: CodingKey {
var stringValue: String

Expand Down Expand Up @@ -48,7 +48,7 @@ extension _Presence: Codable {
JSONObject.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "A presence should at least have a phx_ref"
debugDescription: "A presence should at least have a phx_ref."
)
)
}
Expand All @@ -58,13 +58,13 @@ extension _Presence: Codable {
String.self,
DecodingError.Context(
codingPath: codingPath + [_StringCodingKey("phx_ref")],
debugDescription: "A presence should at least have a phx_ref"
debugDescription: "A presence should at least have a phx_ref."
)
)
}

meta["phx_ref"] = nil
self = _Presence(ref: presenceRef, state: meta)
self = PresenceV2(ref: presenceRef, state: meta)
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -75,33 +75,36 @@ extension _Presence: Codable {
}

public protocol PresenceAction: Sendable, HasRawMessage {
var joins: [String: _Presence] { get }
var leaves: [String: _Presence] { get }
var joins: [String: PresenceV2] { get }
var leaves: [String: PresenceV2] { get }
}

extension PresenceAction {
public func decodeJoins<T: Decodable>(as _: T.Type, ignoreOtherTypes: Bool = true) throws -> [T] {
public func decodeJoins<T: Decodable>(
as _: T.Type = T.self,
ignoreOtherTypes: Bool = true
) throws -> [T] {
if ignoreOtherTypes {
return joins.values.compactMap { try? $0.state.decode(T.self) }
return joins.values.compactMap { try? $0.state.decode(as: T.self) }
}

return try joins.values.map { try $0.state.decode(T.self) }
return try joins.values.map { try $0.state.decode(as: T.self) }
}

public func decodeLeaves<T: Decodable>(
as _: T.Type,
as _: T.Type = T.self,
ignoreOtherTypes: Bool = true
) throws -> [T] {
if ignoreOtherTypes {
return leaves.values.compactMap { try? $0.state.decode(T.self) }
return leaves.values.compactMap { try? $0.state.decode(as: T.self) }
}

return try leaves.values.map { try $0.state.decode(T.self) }
return try leaves.values.map { try $0.state.decode(as: T.self) }
}
}

struct PresenceActionImpl: PresenceAction {
var joins: [String: _Presence]
var leaves: [String: _Presence]
var joins: [String: PresenceV2]
var leaves: [String: PresenceV2]
var rawMessage: RealtimeMessageV2
}
14 changes: 9 additions & 5 deletions Sources/Realtime/V2/RealtimeChannelV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ public actor RealtimeChannelV2 {
)
}

public func broadcast(event: String, message: some Codable) async throws {
try await broadcast(event: event, message: JSONObject(message))
}

public func broadcast(event: String, message: JSONObject) async {
assert(
status == .subscribed,
Expand Down Expand Up @@ -229,7 +233,7 @@ public actor RealtimeChannelV2 {
{
let serverPostgresChanges = try message.payload["response"]?
.objectValue?["postgres_changes"]?
.decode([PostgresJoinConfig].self)
.decode(as: [PostgresJoinConfig].self)

callbackManager.setServerChanges(changes: serverPostgresChanges ?? [])

Expand All @@ -247,7 +251,7 @@ public actor RealtimeChannelV2 {

let ids = message.payload["ids"]?.arrayValue?.compactMap(\.intValue) ?? []

let postgresActions = try data.decode(PostgresActionData.self)
let postgresActions = try data.decode(as: PostgresActionData.self)

let action: AnyAction = switch postgresActions.type {
case "UPDATE":
Expand Down Expand Up @@ -320,12 +324,12 @@ public actor RealtimeChannelV2 {
)

case .presenceDiff:
let joins = try message.payload["joins"]?.decode([String: _Presence].self) ?? [:]
let leaves = try message.payload["leaves"]?.decode([String: _Presence].self) ?? [:]
let joins = try message.payload["joins"]?.decode(as: [String: PresenceV2].self) ?? [:]
let leaves = try message.payload["leaves"]?.decode(as: [String: PresenceV2].self) ?? [:]
callbackManager.triggerPresenceDiffs(joins: joins, leaves: leaves, rawMessage: message)

case .presenceState:
let joins = try message.payload.decode([String: _Presence].self)
let joins = try message.payload.decode(as: [String: PresenceV2].self)
callbackManager.triggerPresenceDiffs(joins: joins, leaves: [:], rawMessage: message)
}
} catch {
Expand Down
16 changes: 11 additions & 5 deletions Sources/_Helpers/AnyJSON/AnyJSON+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,30 @@ extension AnyJSON {
}
}

public func decode<T: Decodable>(_: T.Type, decoder: JSONDecoder = AnyJSON.decoder) throws -> T {
public func decode<T: Decodable>(
as _: T.Type = T.self,
decoder: JSONDecoder = AnyJSON.decoder
) throws -> T {
let data = try AnyJSON.encoder.encode(self)
return try decoder.decode(T.self, from: data)
}
}

extension JSONArray {
public func decode<T: Decodable>(
_: T.Type,
as _: T.Type = T.self,
decoder: JSONDecoder = AnyJSON.decoder
) throws -> [T] {
try AnyJSON.array(self).decode([T].self, decoder: decoder)
try AnyJSON.array(self).decode(as: [T].self, decoder: decoder)
}
}

extension JSONObject {
public func decode<T: Decodable>(_: T.Type, decoder: JSONDecoder = AnyJSON.decoder) throws -> T {
try AnyJSON.object(self).decode(T.self, decoder: decoder)
public func decode<T: Decodable>(
as _: T.Type = T.self,
decoder: JSONDecoder = AnyJSON.decoder
) throws -> T {
try AnyJSON.object(self).decode(as: T.self, decoder: decoder)
}

public init(_ value: some Codable) throws {
Expand Down
4 changes: 2 additions & 2 deletions Tests/RealtimeTests/CallbackManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ final class CallbackManagerTests: XCTestCase {
func testTriggerPresenceDiffs() {
let callbackManager = CallbackManager()

let joins = ["user1": _Presence(ref: "ref", state: [:])]
let leaves = ["user2": _Presence(ref: "ref", state: [:])]
let joins = ["user1": PresenceV2(ref: "ref", state: [:])]
let leaves = ["user2": PresenceV2(ref: "ref", state: [:])]

let receivedAction = LockIsolated(PresenceAction?.none)

Expand Down
2 changes: 1 addition & 1 deletion Tests/_HelpersTests/AnyJSONTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ final class AnyJSONTests: XCTestCase {
]

XCTAssertNoDifference(try AnyJSON(codableValue), json)
XCTAssertNoDifference(codableValue, try json.decode(CodableValue.self))
XCTAssertNoDifference(codableValue, try json.decode(as: CodableValue.self))
}
}

Expand Down
137 changes: 137 additions & 0 deletions docs/migrations/RealtimeV2 Migration Guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
## RealtimeV2 Migration Guide

In this guide we'll walk you through how to migrate from Realtime to the new RealtimeV2.

### Accessing the new client

Instead of `supabase.realtime` use `supabase.realtimeV2`.

### Observing socket connection status

Use `statusChange` property for observing socket connection changes, example:

```swift
for await status in supabase.realtimeV2.statusChange {
// status: disconnected, connecting, or connected
}
```

If you don't need observation, you can access the current status using `supabase.realtimev2.status`.

### Observing channel subscription status

Use `statusChange` property for observing channel subscription status, example:

```swift
let channel = await supabase.realtimeV2.channel("public:messages")

Task {
for status in await channel.statusChange {
// status: unsubscribed, subscribing subscribed, or unsubscribing.
}
}

await channel.subscribe()
```

If you don't need observation, you can access the current status uusing `channel.status`.

### Listening for Postgres Changes

Observe postgres changes using the new `postgresChanges(_:schema:table:filter)` methods.

```swift
let channel = await supabase.realtimeV2.channel("public:messages")

for await insertion in channel.postgresChanges(InsertAction.self, table: "messages") {
let insertedMessage = try insertion.decodeRecord(as: Message.self)
}

for await update in channel.postgresChanges(UpdateAction.self, table: "messages") {
let updateMessage = try update.decodeRecord(as: Message.self)
let oldMessage = try update.decodeOldRecord(as: Message.self)
}

for await deletion in channel.postgresChanges(DeleteAction.self, table: "messages") {
struct Payload: Decodable {
let id: UUID
}

let payload = try deletion.decodeOldRecord(as: Payload.self)
let deletedMessageID = payload.id
}
```

If you wish to listen for all changes, use:

```swift
for change in channel.postgresChanges(AnyAction.self, table: "messages") {
// change: enum with insert, update, and delete cases.
}
```

### Tracking Presence

Use `track(state:)` method for tracking Presence.

```swift
let channel = await supabase.realtimeV2.channel("room")

await channel.track(state: ["user_id": "abc_123"])
```

Or use method that accepts a `Codable` value:

```swift
struct UserPresence: Codable {
let userId: String
}

await channel.track(UserPresence(userId: "abc_123"))
```

Use `untrack()` for when done:

```swift
await channel.untrack()
```

### Listening for Presence Joins and Leaves

Use `presenceChange()` for obsering Presence state changes.

```swift
for await presence in channel.presenceChange() {
let joins = try presence.decodeJoins(as: UserPresence.self) // joins is [UserPresence]
let leaves = try presence.decodeLeaves(as: UserPresence.self) // leaves is [UserPresence]
}
```


### Pushing broadcast messages

Use `broadcast(event:message)` for pushing a broadcast message.

```swift
await channel.broadcast(event: "PING", message: ["timestamp": .double(Date.now.timeIntervalSince1970)])
```

Or use method that accepts a `Codable` value.

```swift
struct PingEventMessage: Codable {
let timestamp: TimeInterval
}

try await channel.broadcast(event: "PING", message: PingEventMessage(timestamp: Date.now.timeIntervalSince1970))
```

### Listening for Broadcast messages

Use `broadcast()` method for observing broadcast events.

```swift
for await event in channel.broadcast(event: "PING") {
let message = try event.decode(as: PingEventMessage.self)
}
```

0 comments on commit 2d523f0

Please sign in to comment.