From 3bf4425f822b0b95c1cdaa73e9d07943e7282713 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 9 Oct 2024 07:04:13 -0300 Subject: [PATCH 1/3] chore(docs): cleanup SlackClone example --- Examples/SlackClone/AppView.swift | 31 +++++----------- Examples/SlackClone/AuthView.swift | 1 - Examples/SlackClone/ChannelListView.swift | 1 - Examples/SlackClone/ChannelStore.swift | 12 +++---- Examples/SlackClone/Dependencies.swift | 1 + Examples/SlackClone/Logger.swift | 44 +++++------------------ Examples/SlackClone/MessageStore.swift | 10 +++--- Examples/SlackClone/MessagesView.swift | 3 +- Examples/SlackClone/Supabase.swift | 4 +-- Examples/SlackClone/UserStore.swift | 20 +++++------ Examples/SlackClone/supabase/config.toml | 2 +- 11 files changed, 43 insertions(+), 86 deletions(-) diff --git a/Examples/SlackClone/AppView.swift b/Examples/SlackClone/AppView.swift index 4442bb6c8..e15aaed5f 100644 --- a/Examples/SlackClone/AppView.swift +++ b/Examples/SlackClone/AppView.swift @@ -5,6 +5,7 @@ // Created by Guilherme Souza on 27/12/23. // +import OSLog import Supabase import SwiftUI @@ -17,13 +18,14 @@ final class AppViewModel { var realtimeConnectionStatus: RealtimeClientV2.Status? init() { - Task { [weak self] in - for await (event, session) in await supabase.auth.authStateChanges { - guard [.signedIn, .signedOut, .initialSession].contains(event) else { return } - self?.session = session + Task { + for await (event, session) in supabase.auth.authStateChanges { + Logger.main.debug("AuthStateChange: \(event.rawValue)") + guard [.signedIn, .signedOut, .initialSession, .tokenRefreshed].contains(event) else { return } + self.session = session if session == nil { - for subscription in await supabase.realtimeV2.subscriptions.values { + for subscription in supabase.channels { await subscription.unsubscribe() } } @@ -31,18 +33,15 @@ final class AppViewModel { } Task { - for await status in await supabase.realtimeV2.statusChange { + for await status in supabase.realtimeV2.statusChange { realtimeConnectionStatus = status } } } } -@MainActor struct AppView: View { @Bindable var model: AppViewModel - let log = LogStore.shared - @State var logPresented = false @ViewBuilder @@ -50,25 +49,11 @@ struct AppView: View { if model.session != nil { NavigationSplitView { ChannelListView(channel: $model.selectedChannel) - .toolbar { - ToolbarItem { - Button("Log") { - logPresented = true - } - } - } } detail: { if let channel = model.selectedChannel { MessagesView(channel: channel).id(channel.id) } } - .sheet(isPresented: $logPresented) { - List { - ForEach(0 ..< log.messages.count, id: \.self) { i in - Text(log.messages[i].description) - } - } - } } else { AuthView() } diff --git a/Examples/SlackClone/AuthView.swift b/Examples/SlackClone/AuthView.swift index e890d43ae..388d04285 100644 --- a/Examples/SlackClone/AuthView.swift +++ b/Examples/SlackClone/AuthView.swift @@ -31,7 +31,6 @@ final class AuthViewModel { } } -@MainActor struct AuthView: View { @Bindable var model = AuthViewModel() diff --git a/Examples/SlackClone/ChannelListView.swift b/Examples/SlackClone/ChannelListView.swift index 627f71c4e..ec6220654 100644 --- a/Examples/SlackClone/ChannelListView.swift +++ b/Examples/SlackClone/ChannelListView.swift @@ -7,7 +7,6 @@ import SwiftUI -@MainActor struct ChannelListView: View { @Bindable var store = Dependencies.shared.channel @Binding var channel: Channel? diff --git a/Examples/SlackClone/ChannelStore.swift b/Examples/SlackClone/ChannelStore.swift index 627095392..e954cf93e 100644 --- a/Examples/SlackClone/ChannelStore.swift +++ b/Examples/SlackClone/ChannelStore.swift @@ -22,10 +22,10 @@ final class ChannelStore { Task { channels = await fetchChannels() - let channel = await supabase.realtimeV2.channel("public:channels") + let channel = supabase.channel("public:channels") - let insertions = await channel.postgresChange(InsertAction.self, table: "channels") - let deletions = await channel.postgresChange(DeleteAction.self, table: "channels") + let insertions = channel.postgresChange(InsertAction.self, table: "channels") + let deletions = channel.postgresChange(DeleteAction.self, table: "channels") await channel.subscribe() @@ -47,7 +47,7 @@ final class ChannelStore { do { let userId = try await supabase.auth.session.user.id let channel = AddChannel(slug: name, createdBy: userId) - try await supabase.database + try await supabase .from("channels") .insert(channel) .execute() @@ -62,7 +62,7 @@ final class ChannelStore { return channel } - let channel: Channel = try await supabase.database + let channel: Channel = try await supabase .from("channels") .select() .eq("id", value: id) @@ -90,7 +90,7 @@ final class ChannelStore { private func fetchChannels() async -> [Channel] { do { - return try await supabase.database.from("channels").select().execute().value + return try await supabase.from("channels").select().execute().value } catch { dump(error) toast = .init(status: .error, title: "Error", description: error.localizedDescription) diff --git a/Examples/SlackClone/Dependencies.swift b/Examples/SlackClone/Dependencies.swift index 12f34bcb2..a8b989d05 100644 --- a/Examples/SlackClone/Dependencies.swift +++ b/Examples/SlackClone/Dependencies.swift @@ -8,6 +8,7 @@ import Foundation import Supabase +@MainActor class Dependencies { static let shared = Dependencies() diff --git a/Examples/SlackClone/Logger.swift b/Examples/SlackClone/Logger.swift index 396b5adf6..e2c823857 100644 --- a/Examples/SlackClone/Logger.swift +++ b/Examples/SlackClone/Logger.swift @@ -10,45 +10,19 @@ import OSLog import Supabase extension Logger { - static let main = Self(subsystem: "com.supabase.SlackClone", category: "app") + static let main = Self(subsystem: "com.supabase.slack-clone", category: "app") + static let supabase = Self(subsystem: "com.supabase.slack-clone", category: "supabase") } -@Observable -final class LogStore: SupabaseLogger { - private let lock = NSLock() - private var loggers: [String: Logger] = [:] - - static let shared = LogStore() - - @MainActor - var messages: [SupabaseLogMessage] = [] - +struct SupaLogger: SupabaseLogger { func log(message: SupabaseLogMessage) { - Task { - await add(message: message) - } + let logger = Logger.supabase - lock.withLock { - if loggers[message.system] == nil { - loggers[message.system] = Logger( - subsystem: "com.supabase.SlackClone.supabase-swift", - category: message.system - ) - } - - let logger = loggers[message.system]! - - switch message.level { - case .debug: logger.debug("\(message, privacy: .public)") - case .error: logger.error("\(message, privacy: .public)") - case .verbose: logger.info("\(message, privacy: .public)") - case .warning: logger.notice("\(message, privacy: .public)") - } + switch message.level { + case .debug: logger.debug("\(message, privacy: .public)") + case .error: logger.error("\(message, privacy: .public)") + case .verbose: logger.info("\(message, privacy: .public)") + case .warning: logger.notice("\(message, privacy: .public)") } } - - @MainActor - private func add(message: SupabaseLogMessage) { - messages.insert(message, at: 0) - } } diff --git a/Examples/SlackClone/MessageStore.swift b/Examples/SlackClone/MessageStore.swift index 66338d2e7..6282a20e6 100644 --- a/Examples/SlackClone/MessageStore.swift +++ b/Examples/SlackClone/MessageStore.swift @@ -103,11 +103,11 @@ final class MessageStore { private init() { Task { - let channel = await supabase.realtimeV2.channel("public:messages") + let channel = supabase.channel("public:messages") - let insertions = await channel.postgresChange(InsertAction.self, table: "messages") - let updates = await channel.postgresChange(UpdateAction.self, table: "messages") - let deletions = await channel.postgresChange(DeleteAction.self, table: "messages") + let insertions = channel.postgresChange(InsertAction.self, table: "messages") + let updates = channel.postgresChange(UpdateAction.self, table: "messages") + let deletions = channel.postgresChange(DeleteAction.self, table: "messages") await channel.subscribe() @@ -176,7 +176,7 @@ final class MessageStore { /// Fetch all messages and their authors. private func fetchMessages(_ channelId: Channel.ID) async throws -> [Message] { - try await supabase.database + try await supabase .from("messages") .select("*,user:user_id(*),channel:channel_id(*)") .eq("channel_id", value: channelId) diff --git a/Examples/SlackClone/MessagesView.swift b/Examples/SlackClone/MessagesView.swift index 968ded30f..73cb89a53 100644 --- a/Examples/SlackClone/MessagesView.swift +++ b/Examples/SlackClone/MessagesView.swift @@ -9,7 +9,6 @@ import Realtime import Supabase import SwiftUI -@MainActor struct MessagesView: View { let store = Dependencies.shared.messages let userStore = Dependencies.shared.users @@ -74,7 +73,7 @@ struct MessagesView: View { channelId: channel.id ) - try await supabase.database.from("messages").insert(message).execute() + try await supabase.from("messages").insert(message).execute() newMessage = "" } catch { dump(error) diff --git a/Examples/SlackClone/Supabase.swift b/Examples/SlackClone/Supabase.swift index efe5d8f5f..2723557ed 100644 --- a/Examples/SlackClone/Supabase.swift +++ b/Examples/SlackClone/Supabase.swift @@ -25,7 +25,7 @@ let supabase = SupabaseClient( supabaseKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0", options: SupabaseClientOptions( db: .init(encoder: encoder, decoder: decoder), - auth: .init(redirectToURL: URL(string: "com.supabase.slack-clone://")), - global: SupabaseClientOptions.GlobalOptions(logger: LogStore.shared) + auth: .init(redirectToURL: URL(string: "com.supabase.slack-clone://login-callback")), + global: SupabaseClientOptions.GlobalOptions(logger: SupaLogger()) ) ) diff --git a/Examples/SlackClone/UserStore.swift b/Examples/SlackClone/UserStore.swift index fa9d38d1d..f14064bc3 100644 --- a/Examples/SlackClone/UserStore.swift +++ b/Examples/SlackClone/UserStore.swift @@ -19,15 +19,15 @@ final class UserStore { private init() { Task { - let channel = await supabase.realtimeV2.channel("public:users") - let changes = await channel.postgresChange(AnyAction.self, table: "users") + let channel = supabase.channel("public:users") + let changes = channel.postgresChange(AnyAction.self, table: "users") - let presences = await channel.presenceChange() + let presences = channel.presenceChange() await channel.subscribe() Task { - let statusChange = await channel.statusChange + let statusChange = channel.statusChange for await _ in statusChange.filter({ $0 == .subscribed }) { let userId = try await supabase.auth.session.user.id try await channel.track(UserPresence(userId: userId, onlineAt: Date())) @@ -45,15 +45,15 @@ final class UserStore { let joins = try presence.decodeJoins(as: UserPresence.self) let leaves = try presence.decodeLeaves(as: UserPresence.self) - for join in joins { - self.presences[join.userId] = join - Logger.main.debug("User \(join.userId) joined") - } - for leave in leaves { self.presences[leave.userId] = nil Logger.main.debug("User \(leave.userId) leaved") } + + for join in joins { + self.presences[join.userId] = join + Logger.main.debug("User \(join.userId) joined") + } } } } @@ -64,7 +64,7 @@ final class UserStore { return user } - let user: User = try await supabase.database + let user: User = try await supabase .from("users") .select() .eq("id", value: id) diff --git a/Examples/SlackClone/supabase/config.toml b/Examples/SlackClone/supabase/config.toml index e886c2f58..a4ee105ff 100644 --- a/Examples/SlackClone/supabase/config.toml +++ b/Examples/SlackClone/supabase/config.toml @@ -71,7 +71,7 @@ enabled = true # in emails. site_url = "http://127.0.0.1:3000" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000", "com.supabase.slack-clone://"] +additional_redirect_urls = ["https://127.0.0.1:3000", "com.supabase.slack-clone://*"] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). jwt_expiry = 3600 # If disabled, the refresh token will never expire. From 6fde879ed70f825ddeaa6468a6e6b33f2205f96e Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 9 Oct 2024 07:32:05 -0300 Subject: [PATCH 2/3] allow to nullify access token --- Sources/Realtime/V2/RealtimeChannelV2.swift | 4 ++-- Sources/Realtime/V2/RealtimeClientV2.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Realtime/V2/RealtimeChannelV2.swift b/Sources/Realtime/V2/RealtimeChannelV2.swift index 431a2ddf6..3191b3b39 100644 --- a/Sources/Realtime/V2/RealtimeChannelV2.swift +++ b/Sources/Realtime/V2/RealtimeChannelV2.swift @@ -193,7 +193,7 @@ public final class RealtimeChannelV2: Sendable { ) } - public func updateAuth(jwt: String) async { + public func updateAuth(jwt: String?) async { logger?.debug("Updating auth token for channel \(topic)") await push( RealtimeMessageV2( @@ -201,7 +201,7 @@ public final class RealtimeChannelV2: Sendable { ref: socket.makeRef().description, topic: topic, event: ChannelEvent.accessToken, - payload: ["access_token": .string(jwt)] + payload: ["access_token": jwt.map { .string($0) } ?? .null] ) ) } diff --git a/Sources/Realtime/V2/RealtimeClientV2.swift b/Sources/Realtime/V2/RealtimeClientV2.swift index 8c5d7a395..b76c98ca5 100644 --- a/Sources/Realtime/V2/RealtimeClientV2.swift +++ b/Sources/Realtime/V2/RealtimeClientV2.swift @@ -347,7 +347,7 @@ public final class RealtimeClientV2: Sendable { } for channel in channels.values { - if let token, channel.status == .subscribed { + if channel.status == .subscribed { await channel.updateAuth(jwt: token) } } From b434e84abdbdc2b2cb44ec77b59983cdeb8e484a Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 9 Oct 2024 08:54:08 -0300 Subject: [PATCH 3/3] deprecate updateAuth from channel --- Sources/Realtime/V2/RealtimeChannelV2.swift | 102 ++++++++----------- Sources/Realtime/V2/RealtimeClientV2.swift | 6 +- Sources/Realtime/V2/RealtimeJoinConfig.swift | 2 + 3 files changed, 50 insertions(+), 60 deletions(-) diff --git a/Sources/Realtime/V2/RealtimeChannelV2.swift b/Sources/Realtime/V2/RealtimeChannelV2.swift index 3191b3b39..437d20a4d 100644 --- a/Sources/Realtime/V2/RealtimeChannelV2.swift +++ b/Sources/Realtime/V2/RealtimeChannelV2.swift @@ -155,13 +155,9 @@ public final class RealtimeChannelV2: Sendable { logger?.debug("Subscribing to channel with body: \(joinConfig)") await push( - RealtimeMessageV2( - joinRef: joinRef, - ref: joinRef, - topic: topic, - event: ChannelEvent.join, - payload: try! JSONObject(payload) - ) + ChannelEvent.join, + ref: joinRef, + payload: try! JSONObject(payload) ) do { @@ -182,27 +178,19 @@ public final class RealtimeChannelV2: Sendable { status = .unsubscribing logger?.debug("Unsubscribing from channel \(topic)") - await push( - RealtimeMessageV2( - joinRef: mutableState.joinRef, - ref: socket.makeRef().description, - topic: topic, - event: ChannelEvent.leave, - payload: [:] - ) - ) + await push(ChannelEvent.leave) } + @available( + *, + deprecated, + message: "manually updating auth token per channel is not recommended, please use `setAuth` in RealtimeClient instead." + ) public func updateAuth(jwt: String?) async { logger?.debug("Updating auth token for channel \(topic)") await push( - RealtimeMessageV2( - joinRef: mutableState.joinRef, - ref: socket.makeRef().description, - topic: topic, - event: ChannelEvent.accessToken, - payload: ["access_token": jwt.map { .string($0) } ?? .null] - ) + ChannelEvent.accessToken, + payload: ["access_token": jwt.map { .string($0) } ?? .null] ) } @@ -264,17 +252,12 @@ public final class RealtimeChannelV2: Sendable { } } else { await push( - RealtimeMessageV2( - joinRef: mutableState.joinRef, - ref: socket.makeRef().description, - topic: topic, - event: ChannelEvent.broadcast, - payload: [ - "type": "broadcast", - "event": .string(event), - "payload": .object(message), - ] - ) + ChannelEvent.broadcast, + payload: [ + "type": "broadcast", + "event": .string(event), + "payload": .object(message), + ] ) } } @@ -290,32 +273,22 @@ public final class RealtimeChannelV2: Sendable { ) await push( - RealtimeMessageV2( - joinRef: mutableState.joinRef, - ref: socket.makeRef().description, - topic: topic, - event: ChannelEvent.presence, - payload: [ - "type": "presence", - "event": "track", - "payload": .object(state), - ] - ) + ChannelEvent.presence, + payload: [ + "type": "presence", + "event": "track", + "payload": .object(state), + ] ) } public func untrack() async { await push( - RealtimeMessageV2( - joinRef: mutableState.joinRef, - ref: socket.makeRef().description, - topic: topic, - event: ChannelEvent.presence, - payload: [ - "type": "presence", - "event": "untrack", - ] - ) + ChannelEvent.presence, + payload: [ + "type": "presence", + "event": "untrack", + ] ) } @@ -572,13 +545,24 @@ public final class RealtimeChannelV2: Sendable { } @discardableResult - private func push(_ message: RealtimeMessageV2) async -> PushStatus { - let push = PushV2(channel: self, message: message) - if let ref = message.ref { - mutableState.withValue { + func push(_ event: String, ref: String? = nil, payload: JSONObject = [:]) async -> PushStatus { + let push = mutableState.withValue { + let message = RealtimeMessageV2( + joinRef: $0.joinRef, + ref: ref ?? socket.makeRef().description, + topic: self.topic, + event: event, + payload: payload + ) + + let push = PushV2(channel: self, message: message) + if let ref = message.ref { $0.pushes[ref] = push } + + return push } + return await push.send() } diff --git a/Sources/Realtime/V2/RealtimeClientV2.swift b/Sources/Realtime/V2/RealtimeClientV2.swift index b76c98ca5..cd4ead9fd 100644 --- a/Sources/Realtime/V2/RealtimeClientV2.swift +++ b/Sources/Realtime/V2/RealtimeClientV2.swift @@ -348,7 +348,11 @@ public final class RealtimeClientV2: Sendable { for channel in channels.values { if channel.status == .subscribed { - await channel.updateAuth(jwt: token) + options.logger?.debug("Updating auth token for channel \(channel.topic)") + await channel.push( + ChannelEvent.accessToken, + payload: ["access_token": token.map { .string($0) } ?? .null] + ) } } } diff --git a/Sources/Realtime/V2/RealtimeJoinConfig.swift b/Sources/Realtime/V2/RealtimeJoinConfig.swift index 55f865304..06b0a4920 100644 --- a/Sources/Realtime/V2/RealtimeJoinConfig.swift +++ b/Sources/Realtime/V2/RealtimeJoinConfig.swift @@ -32,6 +32,7 @@ struct RealtimeJoinConfig: Codable, Hashable { } public struct BroadcastJoinConfig: Codable, Hashable, Sendable { + /// Instructs server to acknowledge that broadcast message was received. public var acknowledgeBroadcasts: Bool = false /// Broadcast messages back to the sender. /// @@ -45,6 +46,7 @@ public struct BroadcastJoinConfig: Codable, Hashable, Sendable { } public struct PresenceJoinConfig: Codable, Hashable, Sendable { + /// Track presence payload across clients. public var key: String = "" }