Skip to content

Commit fabc07d

Browse files
authored
fix(realtime): lost postgres_changes on resubscribe (#585)
1 parent 1176dea commit fabc07d

12 files changed

+258
-265
lines changed

Examples/SlackClone/AppView.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ final class AppViewModel {
1515
var session: Session?
1616
var selectedChannel: Channel?
1717

18-
var realtimeConnectionStatus: RealtimeClientV2.Status?
18+
var realtimeConnectionStatus: RealtimeClientStatus?
1919

2020
init() {
2121
Task {
2222
for await (event, session) in supabase.auth.authStateChanges {
2323
Logger.main.debug("AuthStateChange: \(event.rawValue)")
24-
guard [.signedIn, .signedOut, .initialSession, .tokenRefreshed].contains(event) else { return }
24+
guard [.signedIn, .signedOut, .initialSession, .tokenRefreshed].contains(event) else {
25+
return
26+
}
2527
self.session = session
2628

2729
if session == nil {

Examples/SlackClone/Supabase.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ let decoder: JSONDecoder = {
2121
}()
2222

2323
let supabase = SupabaseClient(
24-
supabaseURL: URL(string: "http://localhost:54321")!,
25-
supabaseKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0",
24+
supabaseURL: URL(string: "http://127.0.0.1:54321")!,
25+
supabaseKey:
26+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0",
2627
options: SupabaseClientOptions(
2728
db: .init(encoder: encoder, decoder: decoder),
2829
auth: .init(redirectToURL: URL(string: "com.supabase.slack-clone://login-callback")),

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ let package = Package(
120120
name: "Realtime",
121121
dependencies: [
122122
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
123+
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
123124
"Helpers",
124125
]
125126
),

Sources/Realtime/V2/CallbackManager.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
//
2-
// CallbackManager.swift
3-
//
4-
//
5-
// Created by Guilherme Souza on 24/12/23.
6-
//
7-
81
import ConcurrencyExtras
92
import Foundation
103
import Helpers
@@ -26,6 +19,10 @@ final class CallbackManager: Sendable {
2619
mutableState.callbacks
2720
}
2821

22+
deinit {
23+
reset()
24+
}
25+
2926
@discardableResult
3027
func addBroadcastCallback(
3128
event: String,

Sources/Realtime/V2/PushV2.swift

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,31 @@ actor PushV2 {
2727
}
2828

2929
func send() async -> PushStatus {
30-
await channel?.socket.push(message)
31-
32-
if channel?.config.broadcast.acknowledgeBroadcasts == true {
33-
do {
34-
return try await withTimeout(interval: channel?.socket.options().timeoutInterval ?? 10) {
35-
await withCheckedContinuation {
36-
self.receivedContinuation = $0
37-
}
30+
guard let channel = channel else {
31+
return .error
32+
}
33+
34+
await channel.socket.push(message)
35+
36+
if !channel.config.broadcast.acknowledgeBroadcasts {
37+
// channel was configured with `ack = false`,
38+
// don't wait for a response and return `ok`.
39+
return .ok
40+
}
41+
42+
do {
43+
return try await withTimeout(interval: channel.socket.options().timeoutInterval) {
44+
await withCheckedContinuation { continuation in
45+
self.receivedContinuation = continuation
3846
}
39-
} catch is TimeoutError {
40-
channel?.logger?.debug("Push timed out.")
41-
return .timeout
42-
} catch {
43-
channel?.logger?.error("Error sending push: \(error)")
44-
return .error
4547
}
48+
} catch is TimeoutError {
49+
channel.logger?.debug("Push timed out.")
50+
return .timeout
51+
} catch {
52+
channel.logger?.error("Error sending push: \(error.localizedDescription)")
53+
return .error
4654
}
47-
48-
return .ok
4955
}
5056

5157
func didReceive(status: PushStatus) {

Sources/Realtime/V2/RealtimeChannelV2.swift

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
//
2-
// RealtimeChannelV2.swift
3-
//
4-
//
5-
// Created by Guilherme Souza on 26/12/23.
6-
//
7-
81
import ConcurrencyExtras
92
import Foundation
103
import HTTPTypes
114
import Helpers
5+
import IssueReporting
126

137
#if canImport(FoundationNetworking)
148
import FoundationNetworking
@@ -123,18 +117,14 @@ public final class RealtimeChannelV2: Sendable {
123117
public func subscribe() async {
124118
if socket.status() != .connected {
125119
if socket.options().connectOnSubscribe != true {
126-
fatalError(
120+
reportIssue(
127121
"You can't subscribe to a channel while the realtime client is not connected. Did you forget to call `realtime.connect()`?"
128122
)
123+
return
129124
}
130125
await socket.connect()
131126
}
132127

133-
guard status != .subscribed else {
134-
logger?.warning("Channel \(topic) is already subscribed")
135-
return
136-
}
137-
138128
socket.addChannel(self)
139129

140130
status = .subscribing
@@ -266,15 +256,21 @@ public final class RealtimeChannelV2: Sendable {
266256
}
267257
}
268258

259+
/// Tracks the given state in the channel.
260+
/// - Parameter state: The state to be tracked, conforming to `Codable`.
261+
/// - Throws: An error if the tracking fails.
269262
public func track(_ state: some Codable) async throws {
270263
try await track(state: JSONObject(state))
271264
}
272265

266+
/// Tracks the given state in the channel.
267+
/// - Parameter state: The state to be tracked as a `JSONObject`.
273268
public func track(state: JSONObject) async {
274-
assert(
275-
status == .subscribed,
276-
"You can only track your presence after subscribing to the channel. Did you forget to call `channel.subscribe()`?"
277-
)
269+
if status != .subscribed {
270+
reportIssue(
271+
"You can only track your presence after subscribing to the channel. Did you forget to call `channel.subscribe()`?"
272+
)
273+
}
278274

279275
await push(
280276
ChannelEvent.presence,
@@ -286,6 +282,7 @@ public final class RealtimeChannelV2: Sendable {
286282
)
287283
}
288284

285+
/// Stops tracking the current state in the channel.
289286
public func untrack() async {
290287
await push(
291288
ChannelEvent.presence,
@@ -520,10 +517,12 @@ public final class RealtimeChannelV2: Sendable {
520517
filter: String?,
521518
callback: @escaping @Sendable (AnyAction) -> Void
522519
) -> RealtimeSubscription {
523-
precondition(
524-
status != .subscribed,
525-
"You cannot call postgresChange after joining the channel"
526-
)
520+
guard status != .subscribed else {
521+
reportIssue(
522+
"You cannot call postgresChange after joining the channel, this won't work as expected."
523+
)
524+
return RealtimeSubscription {}
525+
}
527526

528527
let config = PostgresJoinConfig(
529528
event: event,

Sources/Realtime/V2/RealtimeClientV2.swift

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,13 @@ public final class RealtimeClientV2: Sendable {
2020
var accessToken: String?
2121
var ref = 0
2222
var pendingHeartbeatRef: Int?
23+
24+
/// Long-running task that keeps sending heartbeat messages.
2325
var heartbeatTask: Task<Void, Never>?
26+
27+
/// Long-running task for listening for incoming messages from WebSocket.
2428
var messageTask: Task<Void, Never>?
29+
2530
var connectionTask: Task<Void, Never>?
2631
var channels: [String: RealtimeChannelV2] = [:]
2732
var sendBuffer: [@Sendable () async -> Void] = []
@@ -34,13 +39,14 @@ public final class RealtimeClientV2: Sendable {
3439
let http: any HTTPClientType
3540
let apikey: String?
3641

42+
/// All managed channels indexed by their topics.
3743
public var channels: [String: RealtimeChannelV2] {
3844
mutableState.channels
3945
}
4046

4147
private let statusEventEmitter = EventEmitter<RealtimeClientStatus>(initialEvent: .disconnected)
4248

43-
/// AsyncStream that emits when connection status change.
49+
/// Listen for connection status changes.
4450
///
4551
/// You can also use ``onStatusChange(_:)`` for a closure based method.
4652
public var statusChange: AsyncStream<RealtimeClientStatus> {
@@ -198,6 +204,13 @@ public final class RealtimeClientV2: Sendable {
198204
await connect(reconnect: true)
199205
}
200206

207+
/// Creates a new channel and bind it to this client.
208+
/// - Parameters:
209+
/// - topic: Channel's topic.
210+
/// - options: Configuration options for the channel.
211+
/// - Returns: Channel instance.
212+
///
213+
/// - Note: This method doesn't subscribe to the channel, call ``RealtimeChannelV2/subscribe()`` on the returned channel instance.
201214
public func channel(
202215
_ topic: String,
203216
options: @Sendable (inout RealtimeChannelConfig) -> Void = { _ in }
@@ -223,6 +236,9 @@ public final class RealtimeClientV2: Sendable {
223236
}
224237
}
225238

239+
/// Unsubscribe and removes channel.
240+
///
241+
/// If there is no channel left, client is disconnected.
226242
public func removeChannel(_ channel: RealtimeChannelV2) async {
227243
if channel.status == .subscribed {
228244
await channel.unsubscribe()
@@ -238,6 +254,7 @@ public final class RealtimeClientV2: Sendable {
238254
}
239255
}
240256

257+
/// Unsubscribes and removes all channels.
241258
public func removeAllChannels() async {
242259
await withTaskGroup(of: Void.self) { group in
243260
for channel in channels.values {
@@ -327,15 +344,19 @@ public final class RealtimeClientV2: Sendable {
327344
}
328345
}
329346

330-
public func disconnect() {
347+
/// Disconnects client.
348+
/// - Parameters:
349+
/// - code: A numeric status code to send on disconnect.
350+
/// - reason: A custom reason for the disconnect.
351+
public func disconnect(code: Int? = nil, reason: String? = nil) {
331352
options.logger?.debug("Closing WebSocket connection")
332353
mutableState.withValue {
333354
$0.ref = 0
334355
$0.messageTask?.cancel()
335356
$0.heartbeatTask?.cancel()
336357
$0.connectionTask?.cancel()
337358
}
338-
ws.disconnect()
359+
ws.disconnect(code: code, reason: reason)
339360
status = .disconnected
340361
}
341362

@@ -388,13 +409,14 @@ public final class RealtimeClientV2: Sendable {
388409
try Task.checkCancellation()
389410
try await self?.ws.send(message)
390411
} catch {
391-
self?.options.logger?.error("""
392-
Failed to send message:
393-
\(message)
394-
395-
Error:
396-
\(error)
397-
""")
412+
self?.options.logger?.error(
413+
"""
414+
Failed to send message:
415+
\(message)
416+
417+
Error:
418+
\(error)
419+
""")
398420
}
399421
}
400422

Sources/Realtime/V2/RealtimeMessageV2.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
//
2-
// RealtimeMessageV2.swift
3-
//
4-
//
5-
// Created by Guilherme Souza on 11/01/24.
6-
//
7-
81
import Foundation
92
import Helpers
103

0 commit comments

Comments
 (0)