From acf504c1aae36153d476b8a39e49915acb62c05d Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 7 Oct 2025 15:50:44 -0300 Subject: [PATCH 1/3] chore: remove deprecated code --- Examples/Examples.xcodeproj/project.pbxproj | 30 +- .../xcshareddata/xcschemes/Examples.xcscheme | 2 +- .../xcschemes/SlackClone.xcscheme | 2 +- .../xcschemes/UserManagement.xcscheme | 2 +- Examples/Examples/Examples.entitlements | 6 - Examples/SlackClone/SlackClone.entitlements | 9 +- .../UserManagement.entitlements | 9 +- Sources/Auth/AuthError.swift | 129 -- Sources/Auth/AuthStateChangeListener.swift | 7 +- Sources/Auth/Deprecated.swift | 149 --- Sources/Auth/Types.swift | 36 +- Sources/Helpers/EventEmitter.swift | 5 - Sources/PostgREST/Deprecated.swift | 143 --- .../PostgREST/PostgrestFilterBuilder.swift | 6 +- Sources/PostgREST/PostgrestFilterValue.swift | 5 - Sources/Realtime/Deprecated/Defaults.swift | 108 -- Sources/Realtime/Deprecated/Delegated.swift | 102 -- Sources/Realtime/Deprecated/Deprecated.swift | 80 -- .../Realtime/Deprecated/HeartbeatTimer.swift | 136 --- .../Deprecated/PhoenixTransport.swift | 316 ----- Sources/Realtime/Deprecated/Presence.swift | 417 ------- Sources/Realtime/Deprecated/Push.swift | 265 ---- .../Realtime/Deprecated/RealtimeChannel.swift | 1037 ---------------- .../Realtime/Deprecated/RealtimeClient.swift | 1071 ----------------- .../Realtime/Deprecated/RealtimeMessage.swift | 86 -- .../Realtime/Deprecated/TimeoutTimer.swift | 108 -- .../Realtime/RealtimeChannel+AsyncAwait.swift | 6 - Sources/Realtime/RealtimeChannelV2.swift | 7 - Sources/Realtime/Types.swift | 26 + Sources/Storage/Codable.swift | 6 +- Sources/Storage/Deprecated.swift | 176 --- Sources/Storage/SupabaseStorage.swift | 6 +- Sources/Supabase/Deprecated.swift | 26 - Sources/Supabase/SupabaseClient.swift | 13 +- .../xcshareddata/xcschemes/Auth.xcscheme | 2 +- .../xcshareddata/xcschemes/Functions.xcscheme | 2 +- .../xcshareddata/xcschemes/PostgREST.xcscheme | 2 +- .../xcshareddata/xcschemes/Realtime.xcscheme | 2 +- .../xcshareddata/xcschemes/Storage.xcscheme | 2 +- .../xcshareddata/xcschemes/Supabase.xcscheme | 2 +- Tests/AuthTests/AuthClientTests.swift | 48 +- Tests/AuthTests/RequestsTests.swift | 12 - .../PostgrestIntegrationTests.swift | 2 +- 43 files changed, 81 insertions(+), 4525 deletions(-) delete mode 100644 Sources/Auth/Deprecated.swift delete mode 100644 Sources/PostgREST/Deprecated.swift delete mode 100644 Sources/Realtime/Deprecated/Defaults.swift delete mode 100644 Sources/Realtime/Deprecated/Delegated.swift delete mode 100644 Sources/Realtime/Deprecated/Deprecated.swift delete mode 100644 Sources/Realtime/Deprecated/HeartbeatTimer.swift delete mode 100644 Sources/Realtime/Deprecated/PhoenixTransport.swift delete mode 100644 Sources/Realtime/Deprecated/Presence.swift delete mode 100644 Sources/Realtime/Deprecated/Push.swift delete mode 100644 Sources/Realtime/Deprecated/RealtimeChannel.swift delete mode 100644 Sources/Realtime/Deprecated/RealtimeClient.swift delete mode 100644 Sources/Realtime/Deprecated/RealtimeMessage.swift delete mode 100644 Sources/Realtime/Deprecated/TimeoutTimer.swift delete mode 100644 Sources/Storage/Deprecated.swift delete mode 100644 Sources/Supabase/Deprecated.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 07bb6ff2c..cf0eff6c8 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -436,7 +436,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1510; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 2600; TargetAttributes = { 793895C52954ABFF0044F2B8 = { CreatedOnToolsVersion = 14.1; @@ -625,6 +625,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ELTTE7K8TT; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -654,6 +655,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -699,6 +701,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ELTTE7K8TT; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -721,6 +724,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -737,9 +741,11 @@ CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"Examples/Preview Content\""; - DEVELOPMENT_TEAM = ELTTE7K8TT; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Examples/Info.plist; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -763,9 +769,11 @@ CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"Examples/Preview Content\""; - DEVELOPMENT_TEAM = ELTTE7K8TT; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Examples/Info.plist; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -790,9 +798,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"SlackClone/Preview Content\""; - DEVELOPMENT_TEAM = ELTTE7K8TT; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SlackClone/Info.plist; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; @@ -830,9 +840,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"SlackClone/Preview Content\""; - DEVELOPMENT_TEAM = ELTTE7K8TT; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SlackClone/Info.plist; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; @@ -870,9 +882,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"UserManagement/Preview Content\""; - DEVELOPMENT_TEAM = ELTTE7K8TT; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = UserManagement/Info.plist; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; @@ -910,9 +924,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"UserManagement/Preview Content\""; - DEVELOPMENT_TEAM = ELTTE7K8TT; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = UserManagement/Info.plist; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Examples.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Examples.xcscheme index 34322b901..dcd2d2d2b 100644 --- a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Examples.xcscheme +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Examples.xcscheme @@ -1,6 +1,6 @@ Default - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - com.apple.security.network.client - keychain-access-groups diff --git a/Examples/SlackClone/SlackClone.entitlements b/Examples/SlackClone/SlackClone.entitlements index 625af03d9..0c67376eb 100644 --- a/Examples/SlackClone/SlackClone.entitlements +++ b/Examples/SlackClone/SlackClone.entitlements @@ -1,12 +1,5 @@ - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - com.apple.security.network.client - - + diff --git a/Examples/UserManagement/UserManagement.entitlements b/Examples/UserManagement/UserManagement.entitlements index 625af03d9..0c67376eb 100644 --- a/Examples/UserManagement/UserManagement.entitlements +++ b/Examples/UserManagement/UserManagement.entitlements @@ -1,12 +1,5 @@ - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - com.apple.security.network.client - - + diff --git a/Sources/Auth/AuthError.swift b/Sources/Auth/AuthError.swift index 991100bf5..8163b8fd0 100644 --- a/Sources/Auth/AuthError.swift +++ b/Sources/Auth/AuthError.swift @@ -118,128 +118,6 @@ extension ErrorCode { } public enum AuthError: LocalizedError, Equatable { - @available( - *, - deprecated, - message: - "Error used to be thrown when no exp claim was found in JWT during setSession(accessToken:refreshToken:) method." - ) - case missingExpClaim - - @available( - *, - deprecated, - message: - "Error used to be thrown when provided JWT wasn't valid during setSession(accessToken:refreshToken:) method." - ) - case malformedJWT - - @available(*, deprecated, renamed: "sessionMissing") - public static var sessionNotFound: AuthError { .sessionMissing } - - /// Error thrown during PKCE flow. - @available( - *, - deprecated, - renamed: "pkceGrantCodeExchange", - message: "Error was grouped in `pkceGrantCodeExchange`, please use it instead of `pkce`." - ) - public static func pkce(_ reason: PKCEFailureReason) -> AuthError { - switch reason { - case .codeVerifierNotFound: - .pkceGrantCodeExchange(message: "A code verifier wasn't found in PKCE flow.") - case .invalidPKCEFlowURL: - .pkceGrantCodeExchange(message: "Not a valid PKCE flow url.") - } - } - - @available(*, deprecated, message: "Use `pkceGrantCodeExchange` instead.") - public enum PKCEFailureReason: Sendable { - /// Code verifier not found in the URL. - case codeVerifierNotFound - - /// Not a valid PKCE flow URL. - case invalidPKCEFlowURL - } - - @available(*, deprecated, renamed: "implicitGrantRedirect") - public static var invalidImplicitGrantFlowURL: AuthError { - .implicitGrantRedirect(message: "Not a valid implicit grant flow url.") - } - - @available( - *, - deprecated, - message: - "This error is never thrown, if you depend on it, you can remove the logic as it never happens." - ) - case missingURL - - @available( - *, - deprecated, - message: - "Error used to be thrown on methods which required a valid redirect scheme, such as signInWithOAuth. This is now considered a programming error an a assertion is triggered in case redirect scheme isn't provided." - ) - case invalidRedirectScheme - - @available( - *, - deprecated, - renamed: "api(message:errorCode:underlyingData:underlyingResponse:)" - ) - public static func api(_ error: APIError) -> AuthError { - let message = error.msg ?? error.error ?? error.errorDescription ?? "Unexpected API error." - if let weakPassword = error.weakPassword { - return .weakPassword(message: message, reasons: weakPassword.reasons) - } - - return .api( - message: message, - errorCode: .unknown, - underlyingData: (try? AuthClient.Configuration.jsonEncoder.encode(error)) ?? Data(), - underlyingResponse: HTTPURLResponse( - url: defaultAuthURL, - statusCode: error.code ?? 500, - httpVersion: nil, - headerFields: nil - )! - ) - } - - /// An error returned by the API. - @available( - *, - deprecated, - renamed: "api(message:errorCode:underlyingData:underlyingResponse:)" - ) - public struct APIError: Error, Codable, Sendable, Equatable { - /// A basic message describing the problem with the request. Usually missing if - /// ``AuthError/APIError/error`` is present. - public var msg: String? - - /// The HTTP status code. Usually missing if ``AuthError/APIError/error`` is present. - public var code: Int? - - /// Certain responses will contain this property with the provided values. - /// - /// Usually one of these: - /// - `invalid_request` - /// - `unauthorized_client` - /// - `access_denied` - /// - `server_error` - /// - `temporarily_unavailable` - /// - `unsupported_otp_type` - public var error: String? - - /// Certain responses that have an ``AuthError/APIError/error`` property may have this property - /// which describes the error. - public var errorDescription: String? - - /// Only returned when signing up if the password used is too weak. Inspect the - /// ``WeakPassword/reasons`` and ``AuthError/APIError/msg`` property to identify the causes. - public var weakPassword: WeakPassword? - } /// Error thrown when a session is required to proceed, but none was found, either thrown by the client, or returned by the server. case sessionMissing @@ -274,11 +152,6 @@ public enum AuthError: LocalizedError, Equatable { let .implicitGrantRedirect(message), let .jwtVerificationFailed(message): message - // Deprecated cases - case .missingExpClaim: "Missing expiration claim in the access token." - case .malformedJWT: "A malformed JWT received." - case .invalidRedirectScheme: "Invalid redirect scheme." - case .missingURL: "Missing URL." } } @@ -289,8 +162,6 @@ public enum AuthError: LocalizedError, Equatable { case let .api(_, errorCode, _, _): errorCode case .pkceGrantCodeExchange, .implicitGrantRedirect: .unknown case .jwtVerificationFailed: .invalidJWT - // Deprecated cases - case .missingExpClaim, .malformedJWT, .invalidRedirectScheme, .missingURL: .unknown } } diff --git a/Sources/Auth/AuthStateChangeListener.swift b/Sources/Auth/AuthStateChangeListener.swift index c0d794154..41c09532d 100644 --- a/Sources/Auth/AuthStateChangeListener.swift +++ b/Sources/Auth/AuthStateChangeListener.swift @@ -8,7 +8,6 @@ import ConcurrencyExtras import Foundation - /// A listener that can be removed by calling ``AuthStateChangeListenerRegistration/remove()``. /// /// - Note: Listener is automatically removed on deinit. @@ -17,7 +16,11 @@ public protocol AuthStateChangeListenerRegistration: Sendable { func remove() } -extension ObservationToken: AuthStateChangeListenerRegistration {} +extension ObservationToken: AuthStateChangeListenerRegistration { + public func remove() { + cancel() + } +} public typealias AuthStateChangeListener = @Sendable ( _ event: AuthChangeEvent, diff --git a/Sources/Auth/Deprecated.swift b/Sources/Auth/Deprecated.swift deleted file mode 100644 index 9b0ca5f24..000000000 --- a/Sources/Auth/Deprecated.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// Deprecated.swift -// -// -// Created by Guilherme Souza on 14/12/23. -// - -import Foundation - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -@available(*, deprecated, renamed: "AuthClient") -public typealias GoTrueClient = AuthClient - -@available(*, deprecated, renamed: "AuthMFA") -public typealias GoTrueMFA = AuthMFA - -@available(*, deprecated, renamed: "AuthLocalStorage") -public typealias GoTrueLocalStorage = AuthLocalStorage - -@available(*, deprecated, renamed: "AuthMetaSecurity") -public typealias GoTrueMetaSecurity = AuthMetaSecurity - -@available(*, deprecated, renamed: "AuthError") -public typealias GoTrueError = AuthError - -extension JSONEncoder { - @available( - *, - deprecated, - renamed: "AuthClient.Configuration.jsonEncoder", - message: - "Access to the default JSONEncoder instance moved to AuthClient.Configuration.jsonEncoder" - ) - public static var goTrue: JSONEncoder { - AuthClient.Configuration.jsonEncoder - } -} - -extension JSONDecoder { - @available( - *, - deprecated, - renamed: "AuthClient.Configuration.jsonDecoder", - message: - "Access to the default JSONDecoder instance moved to AuthClient.Configuration.jsonDecoder" - ) - public static var goTrue: JSONDecoder { - AuthClient.Configuration.jsonDecoder - } -} - -extension AuthClient.Configuration { - /// Initializes a AuthClient Configuration with optional parameters. - /// - /// - Parameters: - /// - url: The base URL of the Auth server. - /// - headers: Custom headers to be included in requests. - /// - flowType: The authentication flow type. - /// - localStorage: The storage mechanism for local data. - /// - encoder: The JSON encoder to use for encoding requests. - /// - decoder: The JSON decoder to use for decoding responses. - /// - fetch: The asynchronous fetch handler for network requests. - @available( - *, - deprecated, - message: - "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" - ) - public init( - url: URL, - headers: [String: String] = [:], - flowType: AuthFlowType = Self.defaultFlowType, - localStorage: any AuthLocalStorage, - encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, - decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping AuthClient.FetchHandler = { try await URLSession.shared.data(for: $0) } - ) { - self.init( - url: url, - headers: headers, - flowType: flowType, - localStorage: localStorage, - logger: nil, - encoder: encoder, - decoder: decoder, - fetch: fetch - ) - } -} - -extension AuthClient { - /// Initializes a AuthClient Configuration with optional parameters. - /// - /// - Parameters: - /// - url: The base URL of the Auth server. - /// - headers: Custom headers to be included in requests. - /// - flowType: The authentication flow type. - /// - localStorage: The storage mechanism for local data. - /// - encoder: The JSON encoder to use for encoding requests. - /// - decoder: The JSON decoder to use for decoding responses. - /// - fetch: The asynchronous fetch handler for network requests. - @available( - *, - deprecated, - message: - "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" - ) - public init( - url: URL, - headers: [String: String] = [:], - flowType: AuthFlowType = Configuration.defaultFlowType, - localStorage: any AuthLocalStorage, - encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, - decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping AuthClient.FetchHandler = { try await URLSession.shared.data(for: $0) } - ) { - self.init( - url: url, - headers: headers, - flowType: flowType, - localStorage: localStorage, - logger: nil, - encoder: encoder, - decoder: decoder, - fetch: fetch - ) - } -} - -@available(*, deprecated, message: "Use MFATotpEnrollParams or MFAPhoneEnrollParams instead.") -public typealias MFAEnrollParams = MFATotpEnrollParams - -extension AuthAdmin { - @available( - *, - deprecated, - message: "Use deleteUser with UUID instead of string." - ) - public func deleteUser(id: String, shouldSoftDelete: Bool = false) async throws { - guard let id = UUID(uuidString: id) else { - fatalError("id should be a valid UUID") - } - - try await self.deleteUser(id: id, shouldSoftDelete: shouldSoftDelete) - } -} diff --git a/Sources/Auth/Types.swift b/Sources/Auth/Types.swift index f0400d4e8..1248d0e0c 100644 --- a/Sources/Auth/Types.swift +++ b/Sources/Auth/Types.swift @@ -11,31 +11,12 @@ public enum AuthChangeEvent: String, Sendable { case mfaChallengeVerified = "MFA_CHALLENGE_VERIFIED" } -@available( - *, - deprecated, - message: "Access to UserCredentials will be removed on the next major release." -) -public struct UserCredentials: Codable, Hashable, Sendable { - public var email: String? - public var password: String? - public var phone: String? - public var refreshToken: String? - public var gotrueMetaSecurity: AuthMetaSecurity? - - public init( - email: String? = nil, - password: String? = nil, - phone: String? = nil, - refreshToken: String? = nil, - gotrueMetaSecurity: AuthMetaSecurity? = nil - ) { - self.email = email - self.password = password - self.phone = phone - self.refreshToken = refreshToken - self.gotrueMetaSecurity = gotrueMetaSecurity - } +struct UserCredentials: Codable, Hashable, Sendable { + var email: String? + var password: String? + var phone: String? + var refreshToken: String? + var gotrueMetaSecurity: AuthMetaSecurity? } struct SignUpRequest: Codable, Hashable, Sendable { @@ -479,9 +460,6 @@ public struct UserAttributes: Codable, Hashable, Sendable { /// Note: Call ``AuthClient/reauthenticate()`` to obtain the nonce first. public var nonce: String? - /// An email change token. - @available(*, deprecated, message: "This is an old field, stop relying on it.") - public var emailChangeToken: String? /// A custom data object to store the user's metadata. This maps to the `auth.users.user_metadata` /// column. The `data` should be a JSON object that includes user-specific info, such as their /// first and last name. @@ -495,14 +473,12 @@ public struct UserAttributes: Codable, Hashable, Sendable { phone: String? = nil, password: String? = nil, nonce: String? = nil, - emailChangeToken: String? = nil, data: [String: AnyJSON]? = nil ) { self.email = email self.phone = phone self.password = password self.nonce = nonce - self.emailChangeToken = emailChangeToken self.data = data } } diff --git a/Sources/Helpers/EventEmitter.swift b/Sources/Helpers/EventEmitter.swift index 63d07d978..ac2a98a7d 100644 --- a/Sources/Helpers/EventEmitter.swift +++ b/Sources/Helpers/EventEmitter.swift @@ -23,11 +23,6 @@ public final class ObservationToken: @unchecked Sendable, Hashable { self.onCancel = onCancel } - @available(*, deprecated, renamed: "cancel") - public func remove() { - cancel() - } - public func cancel() { _isCancelled.withValue { isCancelled in guard !isCancelled else { return } diff --git a/Sources/PostgREST/Deprecated.swift b/Sources/PostgREST/Deprecated.swift deleted file mode 100644 index da8fe3459..000000000 --- a/Sources/PostgREST/Deprecated.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// Deprecated.swift -// -// -// Created by Guilherme Souza on 16/01/24. -// - -import Foundation - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -extension PostgrestClient.Configuration { - /// Initializes a new configuration for the PostgREST client. - /// - Parameters: - /// - url: The URL of the PostgREST server. - /// - schema: The schema to use. - /// - headers: The headers to include in requests. - /// - fetch: The fetch handler to use for requests. - /// - encoder: The JSONEncoder to use for encoding. - /// - decoder: The JSONDecoder to use for decoding. - @available( - *, - deprecated, - message: - "Replace usages of this initializer with new init(url:schema:headers:logger:fetch:encoder:decoder:)" - ) - public init( - url: URL, - schema: String? = nil, - headers: [String: String] = [:], - fetch: @escaping PostgrestClient.FetchHandler = { try await URLSession.shared.data(for: $0) }, - encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, - decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder - ) { - self.init( - url: url, - schema: schema, - headers: headers, - logger: nil, - fetch: fetch, - encoder: encoder, - decoder: decoder - ) - } -} - -extension PostgrestClient { - /// Creates a PostgREST client with the specified parameters. - /// - Parameters: - /// - url: The URL of the PostgREST server. - /// - schema: The schema to use. - /// - headers: The headers to include in requests. - /// - session: The URLSession to use for requests. - /// - encoder: The JSONEncoder to use for encoding. - /// - decoder: The JSONDecoder to use for decoding. - @available( - *, - deprecated, - message: - "Replace usages of this initializer with new init(url:schema:headers:logger:fetch:encoder:decoder:)" - ) - public convenience init( - url: URL, - schema: String? = nil, - headers: [String: String] = [:], - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, - encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, - decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder - ) { - self.init( - url: url, - schema: schema, - headers: headers, - logger: nil, - fetch: fetch, - encoder: encoder, - decoder: decoder - ) - } -} - -extension PostgrestFilterBuilder { - - @available(*, deprecated, renamed: "like(_:pattern:)") - public func like( - _ column: String, - value: any PostgrestFilterValue - ) -> PostgrestFilterBuilder { - like(column, pattern: value) - } - - @available(*, deprecated, renamed: "in(_:values:)") - public func `in`( - _ column: String, - value: [any PostgrestFilterValue] - ) -> PostgrestFilterBuilder { - `in`(column, values: value) - } - - @available(*, deprecated, message: "Use textSearch(_:query:config:type) with .plain type.") - public func plfts( - _ column: String, - query: any PostgrestFilterValue, - config: String? = nil - ) -> PostgrestFilterBuilder { - textSearch(column, query: query, config: config, type: .plain) - } - - @available(*, deprecated, message: "Use textSearch(_:query:config:type) with .phrase type.") - public func phfts( - _ column: String, - query: any PostgrestFilterValue, - config: String? = nil - ) -> PostgrestFilterBuilder { - textSearch(column, query: query, config: config, type: .phrase) - } - - @available(*, deprecated, message: "Use textSearch(_:query:config:type) with .websearch type.") - public func wfts( - _ column: String, - query: any PostgrestFilterValue, - config: String? = nil - ) -> PostgrestFilterBuilder { - textSearch(column, query: query, config: config, type: .websearch) - } - - @available(*, deprecated, renamed: "ilike(_:pattern:)") - public func ilike( - _ column: String, - value: any PostgrestFilterValue - ) -> PostgrestFilterBuilder { - ilike(column, pattern: value) - } -} - -@available( - *, - deprecated, - renamed: "PostgrestFilterValue" -) -public typealias URLQueryRepresentable = PostgrestFilterValue diff --git a/Sources/PostgREST/PostgrestFilterBuilder.swift b/Sources/PostgREST/PostgrestFilterBuilder.swift index 02e50df82..0a548113a 100644 --- a/Sources/PostgREST/PostgrestFilterBuilder.swift +++ b/Sources/PostgREST/PostgrestFilterBuilder.swift @@ -575,7 +575,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda query: String, config: String? = nil ) -> PostgrestFilterBuilder { - plfts(column, query: query, config: config) + textSearch(column, query: query, config: config, type: .plain) } public func phraseToFullTextSearch( @@ -583,7 +583,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda query: String, config: String? = nil ) -> PostgrestFilterBuilder { - phfts(column, query: query, config: config) + textSearch(column, query: query, config: config, type: .phrase) } public func webFullTextSearch( @@ -591,6 +591,6 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda query: String, config: String? = nil ) -> PostgrestFilterBuilder { - wfts(column, query: query, config: config) + textSearch(column, query: query, config: config, type: .websearch) } } diff --git a/Sources/PostgREST/PostgrestFilterValue.swift b/Sources/PostgREST/PostgrestFilterValue.swift index 1d26ca7de..1277bee03 100644 --- a/Sources/PostgREST/PostgrestFilterValue.swift +++ b/Sources/PostgREST/PostgrestFilterValue.swift @@ -5,11 +5,6 @@ public protocol PostgrestFilterValue { var rawValue: String { get } } -extension PostgrestFilterValue { - @available(*, deprecated, renamed: "rawValue") - public var queryValue: String { rawValue } -} - extension String: PostgrestFilterValue { public var rawValue: String { self } } diff --git a/Sources/Realtime/Deprecated/Defaults.swift b/Sources/Realtime/Deprecated/Defaults.swift deleted file mode 100644 index e74f08bc7..000000000 --- a/Sources/Realtime/Deprecated/Defaults.swift +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// A collection of default values and behaviors used across the Client -public enum Defaults { - /// Default timeout when sending messages - public static let timeoutInterval: TimeInterval = 10.0 - - /// Default interval to send heartbeats on - public static let heartbeatInterval: TimeInterval = 30.0 - - /// Default maximum amount of time which the system may delay heartbeat events in order to - /// minimize power usage - public static let heartbeatLeeway: DispatchTimeInterval = .milliseconds(10) - - /// Default reconnect algorithm for the socket - public static let reconnectSteppedBackOff: (Int) -> TimeInterval = { tries in - tries > 9 ? 5.0 : [0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.5, 1.0, 2.0][tries - 1] - } - - /** Default rejoin algorithm for individual channels */ - public static let rejoinSteppedBackOff: (Int) -> TimeInterval = { tries in - tries > 3 ? 10 : [1, 2, 5][tries - 1] - } - - public static let vsn = "2.0.0" - - /// Default encode function, utilizing JSONSerialization.data - public static let encode: (Any) -> Data = { json in - try! JSONSerialization - .data( - withJSONObject: json, - options: JSONSerialization.WritingOptions() - ) - } - - /// Default decode function, utilizing JSONSerialization.jsonObject - public static let decode: (Data) -> Any? = { data in - guard - let json = - try? JSONSerialization - .jsonObject( - with: data, - options: JSONSerialization.ReadingOptions() - ) - else { return nil } - return json - } - - public static let heartbeatQueue: DispatchQueue = .init( - label: "com.phoenix.socket.heartbeat" - ) -} - -/// Represents the multiple states that a Channel can be in -/// throughout it's lifecycle. -public enum ChannelState: String { - case closed - case errored - case joined - case joining - case leaving -} - -/// Represents the different events that can be sent through -/// a channel regarding a Channel's lifecycle. -public enum ChannelEvent { - public static let join = "phx_join" - public static let leave = "phx_leave" - public static let close = "phx_close" - public static let error = "phx_error" - public static let reply = "phx_reply" - public static let system = "system" - public static let broadcast = "broadcast" - public static let accessToken = "access_token" - public static let presence = "presence" - public static let presenceDiff = "presence_diff" - public static let presenceState = "presence_state" - public static let postgresChanges = "postgres_changes" - - public static let heartbeat = "heartbeat" - - static func isLifecyleEvent(_ event: String) -> Bool { - switch event { - case join, leave, reply, error, close: true - default: false - } - } -} diff --git a/Sources/Realtime/Deprecated/Delegated.swift b/Sources/Realtime/Deprecated/Delegated.swift deleted file mode 100644 index 6e5489140..000000000 --- a/Sources/Realtime/Deprecated/Delegated.swift +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -/// Provides a memory-safe way of passing callbacks around while not creating -/// retain cycles. This file was copied from https://github.com/dreymonde/Delegated -/// instead of added as a dependency to reduce the number of packages that -/// ship with SwiftPhoenixClient -public struct Delegated { - private(set) var callback: ((Input) -> Output?)? - - public init() {} - - public mutating func delegate( - to target: Target, - with callback: @escaping (Target, Input) -> Output - ) { - self.callback = { [weak target] input in - guard let target else { - return nil - } - return callback(target, input) - } - } - - public func call(_ input: Input) -> Output? { - callback?(input) - } - - public var isDelegateSet: Bool { - callback != nil - } -} - -extension Delegated { - public mutating func stronglyDelegate( - to target: Target, - with callback: @escaping (Target, Input) -> Output - ) { - self.callback = { input in - callback(target, input) - } - } - - public mutating func manuallyDelegate(with callback: @escaping (Input) -> Output) { - self.callback = callback - } - - public mutating func removeDelegate() { - callback = nil - } -} - -extension Delegated where Input == Void { - public mutating func delegate( - to target: Target, - with callback: @escaping (Target) -> Output - ) { - delegate(to: target, with: { target, _ in callback(target) }) - } - - public mutating func stronglyDelegate( - to target: Target, - with callback: @escaping (Target) -> Output - ) { - stronglyDelegate(to: target, with: { target, _ in callback(target) }) - } -} - -extension Delegated where Input == Void { - public func call() -> Output? { - call(()) - } -} - -extension Delegated where Output == Void { - public func call(_ input: Input) { - callback?(input) - } -} - -extension Delegated where Input == Void, Output == Void { - public func call() { - call(()) - } -} diff --git a/Sources/Realtime/Deprecated/Deprecated.swift b/Sources/Realtime/Deprecated/Deprecated.swift deleted file mode 100644 index c0cb2937b..000000000 --- a/Sources/Realtime/Deprecated/Deprecated.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Deprecated.swift -// -// -// Created by Guilherme Souza on 23/12/23. -// - -import Foundation - -@available(*, deprecated, renamed: "RealtimeMessage") -public typealias Message = RealtimeMessage - -extension RealtimeClientV2 { - @available(*, deprecated, renamed: "channels") - public var subscriptions: [String: RealtimeChannelV2] { - channels - } - - @available(*, deprecated, renamed: "RealtimeClientOptions") - public struct Configuration: Sendable { - var url: URL - var apiKey: String - var headers: [String: String] - var heartbeatInterval: TimeInterval - var reconnectDelay: TimeInterval - var timeoutInterval: TimeInterval - var disconnectOnSessionLoss: Bool - var connectOnSubscribe: Bool - var logger: (any SupabaseLogger)? - - public init( - url: URL, - apiKey: String, - headers: [String: String] = [:], - heartbeatInterval: TimeInterval = 15, - reconnectDelay: TimeInterval = 7, - timeoutInterval: TimeInterval = 10, - disconnectOnSessionLoss: Bool = true, - connectOnSubscribe: Bool = true, - logger: (any SupabaseLogger)? = nil - ) { - self.url = url - self.apiKey = apiKey - self.headers = headers - self.heartbeatInterval = heartbeatInterval - self.reconnectDelay = reconnectDelay - self.timeoutInterval = timeoutInterval - self.disconnectOnSessionLoss = disconnectOnSessionLoss - self.connectOnSubscribe = connectOnSubscribe - self.logger = logger - } - } - - @available(*, deprecated, renamed: "RealtimeClientStatus") - public typealias Status = RealtimeClientStatus - - @available(*, deprecated, renamed: "RealtimeClientV2.init(url:options:)") - public convenience init(config: Configuration) { - self.init( - url: config.url, - options: RealtimeClientOptions( - headers: config.headers, - heartbeatInterval: config.heartbeatInterval, - reconnectDelay: config.reconnectDelay, - timeoutInterval: config.timeoutInterval, - disconnectOnSessionLoss: config.disconnectOnSessionLoss, - connectOnSubscribe: config.connectOnSubscribe, - logger: config.logger - ) - ) - } -} - -extension RealtimeChannelV2 { - @available(*, deprecated, renamed: "RealtimeSubscription") - public typealias Subscription = ObservationToken - - @available(*, deprecated, renamed: "RealtimeChannelStatus") - public typealias Status = RealtimeChannelStatus -} diff --git a/Sources/Realtime/Deprecated/HeartbeatTimer.swift b/Sources/Realtime/Deprecated/HeartbeatTimer.swift deleted file mode 100644 index 7bd4ccbf0..000000000 --- a/Sources/Realtime/Deprecated/HeartbeatTimer.swift +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/** - Heartbeat Timer class which manages the lifecycle of the underlying - timer which triggers when a heartbeat should be fired. This heartbeat - runs on it's own Queue so that it does not interfere with the main - queue but guarantees thread safety. - */ - -class HeartbeatTimer { - // ---------------------------------------------------------------------- - - // MARK: - Dependencies - - // ---------------------------------------------------------------------- - // The interval to wait before firing the Timer - let timeInterval: TimeInterval - - /// The maximum amount of time which the system may delay the delivery of the timer events - let leeway: DispatchTimeInterval - - // The DispatchQueue to schedule the timers on - let queue: DispatchQueue - - // UUID which specifies the Timer instance. Verifies that timers are different - let uuid: String = UUID().uuidString - - // ---------------------------------------------------------------------- - - // MARK: - Properties - - // ---------------------------------------------------------------------- - // The underlying, cancelable, resettable, timer. - private var temporaryTimer: (any DispatchSourceTimer)? - // The event handler that is called by the timer when it fires. - private var temporaryEventHandler: (() -> Void)? - - /** - Create a new HeartbeatTimer - - - Parameters: - - timeInterval: Interval to fire the timer. Repeats - - queue: Queue to schedule the timer on - - leeway: The maximum amount of time which the system may delay the delivery of the timer events - */ - init( - timeInterval: TimeInterval, queue: DispatchQueue = Defaults.heartbeatQueue, - leeway: DispatchTimeInterval = Defaults.heartbeatLeeway - ) { - self.timeInterval = timeInterval - self.queue = queue - self.leeway = leeway - } - - /** - Create a new HeartbeatTimer - - - Parameter timeInterval: Interval to fire the timer. Repeats - */ - convenience init(timeInterval: TimeInterval) { - self.init(timeInterval: timeInterval, queue: Defaults.heartbeatQueue) - } - - func start(eventHandler: @escaping () -> Void) { - queue.sync { - // Create a new DispatchSourceTimer, passing the event handler - let timer = DispatchSource.makeTimerSource(flags: [], queue: queue) - timer.setEventHandler(handler: eventHandler) - - // Schedule the timer to first fire in `timeInterval` and then - // repeat every `timeInterval` - timer.schedule( - deadline: DispatchTime.now() + self.timeInterval, - repeating: self.timeInterval, - leeway: self.leeway - ) - - // Start the timer - timer.resume() - self.temporaryEventHandler = eventHandler - self.temporaryTimer = timer - } - } - - func stop() { - // Must be queued synchronously to prevent threading issues. - queue.sync { - // DispatchSourceTimer will automatically cancel when released - temporaryTimer = nil - temporaryEventHandler = nil - } - } - - /** - True if the Timer exists and has not been cancelled. False otherwise - */ - var isValid: Bool { - guard let timer = temporaryTimer else { return false } - return !timer.isCancelled - } - - /** - Calls the Timer's event handler immediately. This method - is primarily used in tests (not ideal) - */ - func fire() { - guard isValid else { return } - temporaryEventHandler?() - } -} - -extension HeartbeatTimer: Equatable { - static func == (lhs: HeartbeatTimer, rhs: HeartbeatTimer) -> Bool { - lhs.uuid == rhs.uuid - } -} diff --git a/Sources/Realtime/Deprecated/PhoenixTransport.swift b/Sources/Realtime/Deprecated/PhoenixTransport.swift deleted file mode 100644 index 79c854005..000000000 --- a/Sources/Realtime/Deprecated/PhoenixTransport.swift +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -// ---------------------------------------------------------------------- - -// MARK: - Transport Protocol - -// ---------------------------------------------------------------------- -/** - Defines a `Socket`'s Transport layer. - */ -// sourcery: AutoMockable -public protocol PhoenixTransport { - /// The current `ReadyState` of the `Transport` layer - var readyState: PhoenixTransportReadyState { get } - - /// Delegate for the `Transport` layer - var delegate: (any PhoenixTransportDelegate)? { get set } - - /** - Connect to the server - - - Parameters: - - headers: Headers to include in the URLRequests when opening the Websocket connection. Can be empty [:] - */ - func connect(with headers: [String: String]) - - /** - Disconnect from the server. - - - Parameters: - - code: Status code as defined by Section 7.4 of RFC 6455. - - reason: Reason why the connection is closing. Optional. - */ - func disconnect(code: Int, reason: String?) - - /** - Sends a message to the server. - - - Parameter data: Data to send. - */ - func send(data: Data) -} - -// ---------------------------------------------------------------------- - -// MARK: - Transport Delegate Protocol - -// ---------------------------------------------------------------------- -/// Delegate to receive notifications of events that occur in the `Transport` layer -public protocol PhoenixTransportDelegate { - /** - Notified when the `Transport` opens. - - - Parameter response: Response from the server indicating that the WebSocket handshake was successful and the connection has been upgraded to webSockets - */ - func onOpen(response: URLResponse?) - - /** - Notified when the `Transport` receives an error. - - - Parameter error: Client-side error from the underlying `Transport` implementation - - Parameter response: Response from the server, if any, that occurred with the Error - - */ - func onError(error: any Error, response: URLResponse?) - - /** - Notified when the `Transport` receives a message from the server. - - - Parameter message: Message received from the server - */ - func onMessage(message: String) - - /** - Notified when the `Transport` closes. - - - Parameter code: Code that was sent when the `Transport` closed - - Parameter reason: A concise human-readable prose explanation for the closure - */ - func onClose(code: Int, reason: String?) -} - -// ---------------------------------------------------------------------- - -// MARK: - Transport Ready State Enum - -// ---------------------------------------------------------------------- -/// Available `ReadyState`s of a `Transport` layer. -public enum PhoenixTransportReadyState { - /// The `Transport` is opening a connection to the server. - case connecting - - /// The `Transport` is connected to the server. - case open - - /// The `Transport` is closing the connection to the server. - case closing - - /// The `Transport` has disconnected from the server. - case closed -} - -// ---------------------------------------------------------------------- - -// MARK: - Default Websocket Transport Implementation - -// ---------------------------------------------------------------------- -/// A `Transport` implementation that relies on URLSession's native WebSocket -/// implementation. -/// -/// This implementation ships default with SwiftPhoenixClient however -/// SwiftPhoenixClient supports earlier OS versions using one of the submodule -/// `Transport` implementations. Or you can create your own implementation using -/// your own WebSocket library or implementation. -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) -open class URLSessionTransport: NSObject, PhoenixTransport, URLSessionWebSocketDelegate { - /// The URL to connect to - let url: URL - - /// The URLSession configuration - let configuration: URLSessionConfiguration - - /// The underling URLSession. Assigned during `connect()` - private var session: URLSession? = nil - - /// The ongoing task. Assigned during `connect()` - private var task: URLSessionWebSocketTask? = nil - - /** - Initializes a `Transport` layer built using URLSession's WebSocket - - Example: - - ```swift - let url = URL("wss://example.com/socket") - let transport: Transport = URLSessionTransport(url: url) - ``` - - Using a custom `URLSessionConfiguration` - - ```swift - let url = URL("wss://example.com/socket") - let configuration = URLSessionConfiguration.default - let transport: Transport = URLSessionTransport(url: url, configuration: configuration) - ``` - - - parameter url: URL to connect to - - parameter configuration: Provide your own URLSessionConfiguration. Uses `.default` if none provided - */ - public init(url: URL, configuration: URLSessionConfiguration = .default) { - // URLSession requires that the endpoint be "wss" instead of "https". - let endpoint = url.absoluteString - let wsEndpoint = - endpoint - .replacingOccurrences(of: "http://", with: "ws://") - .replacingOccurrences(of: "https://", with: "wss://") - - // Force unwrapping should be safe here since a valid URL came in and we just - // replaced the protocol. - self.url = URL(string: wsEndpoint)! - self.configuration = configuration - - super.init() - } - - // MARK: - Transport - - public var readyState: PhoenixTransportReadyState = .closed - public var delegate: (any PhoenixTransportDelegate)? = nil - - public func connect(with headers: [String: String]) { - // Set the transport state as connecting - readyState = .connecting - - // Create the session and websocket task - session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) - var request = URLRequest(url: url) - - for (key, value) in headers { - guard let value = value as? String else { continue } - request.addValue(value, forHTTPHeaderField: key) - } - - task = session?.webSocketTask(with: request) - - // Start the task - task?.resume() - } - - open func disconnect(code: Int, reason: String?) { - /* - TODO: - 1. Provide a "strict" mode that fails if an invalid close code is given - 2. If strict mode is disabled, default to CloseCode.invalid - 3. Provide default .normalClosure function - */ - guard let closeCode = URLSessionWebSocketTask.CloseCode(rawValue: code) else { - fatalError("Could not create a CloseCode with invalid code: [\(code)].") - } - - readyState = .closing - task?.cancel(with: closeCode, reason: reason?.data(using: .utf8)) - session?.finishTasksAndInvalidate() - } - - open func send(data: Data) { - Task { - try? await task?.send(.string(String(data: data, encoding: .utf8)!)) - } - } - - // MARK: - URLSessionWebSocketDelegate - - open func urlSession( - _: URLSession, - webSocketTask: URLSessionWebSocketTask, - didOpenWithProtocol _: String? - ) { - // The Websocket is connected. Set Transport state to open and inform delegate - readyState = .open - delegate?.onOpen(response: webSocketTask.response) - - // Start receiving messages - receive() - } - - open func urlSession( - _: URLSession, - webSocketTask _: URLSessionWebSocketTask, - didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, - reason: Data? - ) { - // A close frame was received from the server. - readyState = .closed - delegate?.onClose( - code: closeCode.rawValue, reason: reason.flatMap { String(data: $0, encoding: .utf8) } - ) - } - - open func urlSession( - _: URLSession, - task: URLSessionTask, - didCompleteWithError error: (any Error)? - ) { - // The task has terminated. Inform the delegate that the transport has closed abnormally - // if this was caused by an error. - guard let err = error else { return } - - abnormalErrorReceived(err, response: task.response) - } - - // MARK: - Private - - private func receive() { - Task { - do { - let result = try await task?.receive() - switch result { - case .data: - print("Data received. This method is unsupported by the Client") - case let .string(text): - self.delegate?.onMessage(message: text) - default: - fatalError("Unknown result was received. [\(String(describing: result))]") - } - - // Since `.receive()` is only good for a single message, it must - // be called again after a message is received in order to - // received the next message. - self.receive() - } catch { - print("Error when receiving \(error)") - self.abnormalErrorReceived(error, response: nil) - } - } - } - - private func abnormalErrorReceived(_ error: any Error, response: URLResponse?) { - // Set the state of the Transport to closed - readyState = .closed - - // Inform the Transport's delegate that an error occurred. - delegate?.onError(error: error, response: response) - - // An abnormal error is results in an abnormal closure, such as internet getting dropped - // so inform the delegate that the Transport has closed abnormally. This will kick off - // the reconnect logic. - delegate?.onClose( - code: RealtimeClient.CloseCode.abnormal.rawValue, reason: error.localizedDescription - ) - } -} diff --git a/Sources/Realtime/Deprecated/Presence.swift b/Sources/Realtime/Deprecated/Presence.swift deleted file mode 100644 index 2370697f7..000000000 --- a/Sources/Realtime/Deprecated/Presence.swift +++ /dev/null @@ -1,417 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// The Presence object provides features for syncing presence information from -/// the server with the client and handling presences joining and leaving. -/// -/// ## Syncing state from the server -/// -/// To sync presence state from the server, first instantiate an object and pass -/// your channel in to track lifecycle events: -/// -/// let channel = socket.channel("some:topic") -/// let presence = Presence(channel) -/// -/// If you have custom syncing state events, you can configure the `Presence` -/// object to use those instead. -/// -/// let options = Options(events: [.state: "my_state", .diff: "my_diff"]) -/// let presence = Presence(channel, opts: options) -/// -/// Next, use the presence.onSync callback to react to state changes from the -/// server. For example, to render the list of users every time the list -/// changes, you could write: -/// -/// presence.onSync { renderUsers(presence.list()) } -/// -/// ## Listing Presences -/// -/// presence.list is used to return a list of presence information based on the -/// local state of metadata. By default, all presence metadata is returned, but -/// a listBy function can be supplied to allow the client to select which -/// metadata to use for a given presence. For example, you may have a user -/// online from different devices with a metadata status of "online", but they -/// have set themselves to "away" on another device. In this case, the app may -/// choose to use the "away" status for what appears on the UI. The example -/// below defines a listBy function which prioritizes the first metadata which -/// was registered for each user. This could be the first tab they opened, or -/// the first device they came online from: -/// -/// let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in -/// let first = pres["metas"]!.first! -/// first["count"] = pres["metas"]!.count -/// first["id"] = id -/// return first -/// } -/// let onlineUsers = presence.list(by: listBy) -/// -/// (NOTE: The underlying behavior is a `map` on the `presence.state`. You are -/// mapping the `state` dictionary into whatever datastructure suites your needs) -/// -/// ## Handling individual presence join and leave events -/// -/// The presence.onJoin and presence.onLeave callbacks can be used to react to -/// individual presences joining and leaving the app. For example: -/// -/// let presence = Presence(channel) -/// presence.onJoin { [weak self] (key, current, newPres) in -/// if let cur = current { -/// print("user additional presence", cur) -/// } else { -/// print("user entered for the first time", newPres) -/// } -/// } -/// -/// presence.onLeave { [weak self] (key, current, leftPres) in -/// if current["metas"]?.isEmpty == true { -/// print("user has left from all devices", leftPres) -/// } else { -/// print("user left from a device", current) -/// } -/// } -/// -/// presence.onSync { renderUsers(presence.list()) } -@available( - *, - deprecated, - renamed: "PresenceV2", - message: "Presence class is deprecated in favor of PresenceV2. See migration guide: https://github.com/supabase-community/supabase-swift/blob/main/docs/migrations/RealtimeV2%20Migration%20Guide.md" -) -public final class Presence { - // ---------------------------------------------------------------------- - - // MARK: - Enums and Structs - - // ---------------------------------------------------------------------- - /// Custom options that can be provided when creating Presence - /// - /// ### Example: - /// - /// let options = Options(events: [.state: "my_state", .diff: "my_diff"]) - /// let presence = Presence(channel, opts: options) - public struct Options { - let events: [Events: String] - - /// Default set of Options used when creating Presence. Uses the - /// phoenix events "presence_state" and "presence_diff" - public static let defaults = Options(events: [ - .state: "presence_state", - .diff: "presence_diff", - ]) - - public init(events: [Events: String]) { - self.events = events - } - } - - /// Presense Events - public enum Events: String { - case state - case diff - } - - // ---------------------------------------------------------------------- - - // MARK: - Typaliases - - // ---------------------------------------------------------------------- - /// Meta details of a Presence. Just a dictionary of properties - public typealias Meta = [String: Any] - - /// A mapping of a String to an array of Metas. e.g. {"metas": [{id: 1}]} - public typealias Map = [String: [Meta]] - - /// A mapping of a Presence state to a mapping of Metas - public typealias State = [String: Map] - - // Diff has keys "joins" and "leaves", pointing to a Presence.State each - // containing the users that joined and left. - public typealias Diff = [String: State] - - /// Closure signature of OnJoin callbacks - public typealias OnJoin = (_ key: String, _ current: Map?, _ new: Map) -> Void - - /// Closure signature for OnLeave callbacks - public typealias OnLeave = (_ key: String, _ current: Map, _ left: Map) -> Void - - //// Closure signature for OnSync callbacks - public typealias OnSync = () -> Void - - /// Collection of callbacks with default values - struct Caller { - var onJoin: OnJoin = { _, _, _ in } - var onLeave: OnLeave = { _, _, _ in } - var onSync: OnSync = {} - } - - // ---------------------------------------------------------------------- - - // MARK: - Properties - - // ---------------------------------------------------------------------- - /// The channel the Presence belongs to - weak var channel: RealtimeChannel? - - /// Caller to callback hooks - var caller: Caller - - /// The state of the Presence - public private(set) var state: State - - /// Pending `join` and `leave` diffs that need to be synced - public private(set) var pendingDiffs: [Diff] - - /// The channel's joinRef, set when state events occur - public private(set) var joinRef: String? - - public var isPendingSyncState: Bool { - guard let safeJoinRef = joinRef else { return true } - return safeJoinRef != channel?.joinRef - } - - /// Callback to be informed of joins - public var onJoin: OnJoin { - get { caller.onJoin } - set { caller.onJoin = newValue } - } - - /// Set the OnJoin callback - public func onJoin(_ callback: @escaping OnJoin) { - onJoin = callback - } - - /// Callback to be informed of leaves - public var onLeave: OnLeave { - get { caller.onLeave } - set { caller.onLeave = newValue } - } - - /// Set the OnLeave callback - public func onLeave(_ callback: @escaping OnLeave) { - onLeave = callback - } - - /// Callback to be informed of synces - public var onSync: OnSync { - get { caller.onSync } - set { caller.onSync = newValue } - } - - /// Set the OnSync callback - public func onSync(_ callback: @escaping OnSync) { - onSync = callback - } - - public init(channel: RealtimeChannel, opts: Options = Options.defaults) { - state = [:] - pendingDiffs = [] - self.channel = channel - joinRef = nil - caller = Caller() - - guard // Do not subscribe to events if they were not provided - let stateEvent = opts.events[.state], - let diffEvent = opts.events[.diff] - else { return } - - self.channel?.delegateOn(stateEvent, filter: ChannelFilter(), to: self) { (self, message) in - guard let newState = message.rawPayload as? State else { return } - - self.joinRef = self.channel?.joinRef - self.state = Presence.syncState( - self.state, - newState: newState, - onJoin: self.caller.onJoin, - onLeave: self.caller.onLeave - ) - - for diff in self.pendingDiffs { - self.state = Presence.syncDiff( - self.state, - diff: diff, - onJoin: self.caller.onJoin, - onLeave: self.caller.onLeave - ) - } - - self.pendingDiffs = [] - self.caller.onSync() - } - - self.channel?.delegateOn(diffEvent, filter: ChannelFilter(), to: self) { (self, message) in - guard let diff = message.rawPayload as? Diff else { return } - if self.isPendingSyncState { - self.pendingDiffs.append(diff) - } else { - self.state = Presence.syncDiff( - self.state, - diff: diff, - onJoin: self.caller.onJoin, - onLeave: self.caller.onLeave - ) - self.caller.onSync() - } - } - } - - /// Returns the array of presences, with deault selected metadata. - public func list() -> [Map] { - list(by: { _, pres in pres }) - } - - /// Returns the array of presences, with selected metadata - public func list(by transformer: (String, Map) -> T) -> [T] { - Presence.listBy(state, transformer: transformer) - } - - /// Filter the Presence state with a given function - public func filter(by filter: ((String, Map) -> Bool)?) -> State { - Presence.filter(state, by: filter) - } - - // ---------------------------------------------------------------------- - - // MARK: - Static - - // ---------------------------------------------------------------------- - - // Used to sync the list of presences on the server - // with the client's state. An optional `onJoin` and `onLeave` callback can - // be provided to react to changes in the client's local presences across - // disconnects and reconnects with the server. - // - // - returns: Presence.State - @discardableResult - public static func syncState( - _ currentState: State, - newState: State, - onJoin: OnJoin = { _, _, _ in }, - onLeave: OnLeave = { _, _, _ in } - ) -> State { - let state = currentState - var leaves: Presence.State = [:] - var joins: Presence.State = [:] - - for (key, presence) in state { - if newState[key] == nil { - leaves[key] = presence - } - } - - for (key, newPresence) in newState { - if let currentPresence = state[key] { - let newRefs = newPresence["metas"]!.map { $0["phx_ref"] as! String } - let curRefs = currentPresence["metas"]!.map { $0["phx_ref"] as! String } - - let joinedMetas = newPresence["metas"]!.filter { (meta: Meta) -> Bool in - !curRefs.contains { $0 == meta["phx_ref"] as! String } - } - let leftMetas = currentPresence["metas"]!.filter { (meta: Meta) -> Bool in - !newRefs.contains { $0 == meta["phx_ref"] as! String } - } - - if joinedMetas.count > 0 { - joins[key] = newPresence - joins[key]!["metas"] = joinedMetas - } - - if leftMetas.count > 0 { - leaves[key] = currentPresence - leaves[key]!["metas"] = leftMetas - } - } else { - joins[key] = newPresence - } - } - - return Presence.syncDiff( - state, - diff: ["joins": joins, "leaves": leaves], - onJoin: onJoin, - onLeave: onLeave - ) - } - - // Used to sync a diff of presence join and leave - // events from the server, as they happen. Like `syncState`, `syncDiff` - // accepts optional `onJoin` and `onLeave` callbacks to react to a user - // joining or leaving from a device. - // - // - returns: Presence.State - @discardableResult - public static func syncDiff( - _ currentState: State, - diff: Diff, - onJoin: OnJoin = { _, _, _ in }, - onLeave: OnLeave = { _, _, _ in } - ) -> State { - var state = currentState - diff["joins"]?.forEach { key, newPresence in - let currentPresence = state[key] - state[key] = newPresence - - if let curPresence = currentPresence { - let joinedRefs = state[key]!["metas"]!.map { $0["phx_ref"] as! String } - let curMetas = curPresence["metas"]!.filter { (meta: Meta) -> Bool in - !joinedRefs.contains { $0 == meta["phx_ref"] as! String } - } - state[key]!["metas"]!.insert(contentsOf: curMetas, at: 0) - } - - onJoin(key, currentPresence, newPresence) - } - - diff["leaves"]?.forEach { key, leftPresence in - guard var curPresence = state[key] else { return } - let refsToRemove = leftPresence["metas"]!.map { $0["phx_ref"] as! String } - let keepMetas = curPresence["metas"]!.filter { (meta: Meta) -> Bool in - !refsToRemove.contains { $0 == meta["phx_ref"] as! String } - } - - curPresence["metas"] = keepMetas - onLeave(key, curPresence, leftPresence) - - if keepMetas.count > 0 { - state[key]!["metas"] = keepMetas - } else { - state.removeValue(forKey: key) - } - } - - return state - } - - public static func filter( - _ presences: State, - by filter: ((String, Map) -> Bool)? - ) -> State { - let safeFilter = filter ?? { _, _ in true } - return presences.filter(safeFilter) - } - - public static func listBy( - _ presences: State, - transformer: (String, Map) -> T - ) -> [T] { - presences.map(transformer) - } -} diff --git a/Sources/Realtime/Deprecated/Push.swift b/Sources/Realtime/Deprecated/Push.swift deleted file mode 100644 index 7f681b6da..000000000 --- a/Sources/Realtime/Deprecated/Push.swift +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// Represnts pushing data to a `Channel` through the `Socket` -public class Push { - /// The channel sending the Push - public weak var channel: RealtimeChannel? - - /// The event, for example `phx_join` - public let event: String - - /// The payload, for example ["user_id": "abc123"] - public var payload: Payload - - /// The push timeout. Default is 10.0 seconds - public var timeout: TimeInterval - - /// The server's response to the Push - var receivedMessage: RealtimeMessage? - - /// Timer which triggers a timeout event - var timeoutTimer: TimerQueue - - /// WorkItem to be performed when the timeout timer fires - var timeoutWorkItem: DispatchWorkItem? - - /// Hooks into a Push. Where .receive("ok", callback(Payload)) are stored - var receiveHooks: [PushStatus: [Delegated]] - - /// True if the Push has been sent - var sent: Bool - - /// The reference ID of the Push - var ref: String? - - /// The event that is associated with the reference ID of the Push - var refEvent: String? - - /// Initializes a Push - /// - /// - parameter channel: The Channel - /// - parameter event: The event, for example ChannelEvent.join - /// - parameter payload: Optional. The Payload to send, e.g. ["user_id": "abc123"] - /// - parameter timeout: Optional. The push timeout. Default is 10.0s - init( - channel: RealtimeChannel, - event: String, - payload: Payload = [:], - timeout: TimeInterval = Defaults.timeoutInterval - ) { - self.channel = channel - self.event = event - self.payload = payload - self.timeout = timeout - receivedMessage = nil - timeoutTimer = TimerQueue.main - receiveHooks = [:] - sent = false - ref = nil - } - - /// Resets and sends the Push - /// - parameter timeout: Optional. The push timeout. Default is 10.0s - public func resend(_ timeout: TimeInterval = Defaults.timeoutInterval) { - self.timeout = timeout - reset() - send() - } - - /// Sends the Push. If it has already timed out, then the call will - /// be ignored and return early. Use `resend` in this case. - public func send() { - guard !hasReceived(status: .timeout) else { return } - - startTimeout() - sent = true - channel?.socket?.push( - topic: channel?.topic ?? "", - event: event, - payload: payload, - ref: ref, - joinRef: channel?.joinRef - ) - } - - /// Receive a specific event when sending an Outbound message. Subscribing - /// to status events with this method does not guarantees no retain cycles. - /// You should pass `weak self` in the capture list of the callback. You - /// can call `.delegateReceive(status:, to:, callback:) and the library will - /// handle it for you. - /// - /// Example: - /// - /// channel - /// .send(event:"custom", payload: ["body": "example"]) - /// .receive("error") { [weak self] payload in - /// print("Error: ", payload) - /// } - /// - /// - parameter status: Status to receive - /// - parameter callback: Callback to fire when the status is recevied - @discardableResult - public func receive( - _ status: PushStatus, - callback: @escaping ((RealtimeMessage) -> Void) - ) -> Push { - var delegated = Delegated() - delegated.manuallyDelegate(with: callback) - - return receive(status, delegated: delegated) - } - - /// Receive a specific event when sending an Outbound message. Automatically - /// prevents retain cycles. See `manualReceive(status:, callback:)` if you - /// want to handle this yourself. - /// - /// Example: - /// - /// channel - /// .send(event:"custom", payload: ["body": "example"]) - /// .delegateReceive("error", to: self) { payload in - /// print("Error: ", payload) - /// } - /// - /// - parameter status: Status to receive - /// - parameter owner: The class that is calling .receive. Usually `self` - /// - parameter callback: Callback to fire when the status is recevied - @discardableResult - public func delegateReceive( - _ status: PushStatus, - to owner: Target, - callback: @escaping ((Target, RealtimeMessage) -> Void) - ) -> Push { - var delegated = Delegated() - delegated.delegate(to: owner, with: callback) - - return receive(status, delegated: delegated) - } - - /// Shared behavior between `receive` calls - @discardableResult - func receive(_ status: PushStatus, delegated: Delegated) -> Push { - // If the message has already been received, pass it to the callback immediately - if hasReceived(status: status), let receivedMessage { - delegated.call(receivedMessage) - } - - if receiveHooks[status] == nil { - /// Create a new array of hooks if no previous hook is associated with status - receiveHooks[status] = [delegated] - } else { - /// A previous hook for this status already exists. Just append the new hook - receiveHooks[status]?.append(delegated) - } - - return self - } - - /// Resets the Push as it was after it was first tnitialized. - func reset() { - cancelRefEvent() - ref = nil - refEvent = nil - receivedMessage = nil - sent = false - } - - /// Finds the receiveHook which needs to be informed of a status response - /// - /// - parameter status: Status which was received, e.g. "ok", "error", "timeout" - /// - parameter response: Response that was received - private func matchReceive(_ status: PushStatus, message: RealtimeMessage) { - receiveHooks[status]?.forEach { $0.call(message) } - } - - /// Reverses the result on channel.on(ChannelEvent, callback) that spawned the Push - private func cancelRefEvent() { - guard let refEvent else { return } - channel?.off(refEvent) - } - - /// Cancel any ongoing Timeout Timer - func cancelTimeout() { - timeoutWorkItem?.cancel() - timeoutWorkItem = nil - } - - /// Starts the Timer which will trigger a timeout after a specific _timeout_ - /// time, in milliseconds, is reached. - func startTimeout() { - // Cancel any existing timeout before starting a new one - if let safeWorkItem = timeoutWorkItem, !safeWorkItem.isCancelled { - cancelTimeout() - } - - guard - let channel, - let socket = channel.socket - else { return } - - let ref = socket.makeRef() - let refEvent = channel.replyEventName(ref) - - self.ref = ref - self.refEvent = refEvent - - /// If a response is received before the Timer triggers, cancel timer - /// and match the received event to it's corresponding hook - channel.delegateOn(refEvent, filter: ChannelFilter(), to: self) { (self, message) in - self.cancelRefEvent() - self.cancelTimeout() - self.receivedMessage = message - - /// Check if there is event a status available - guard let status = message.status else { return } - self.matchReceive(status, message: message) - } - - /// Setup and start the Timeout timer. - let workItem = DispatchWorkItem { - self.trigger(.timeout, payload: [:]) - } - - timeoutWorkItem = workItem - timeoutTimer.queue(timeInterval: timeout, execute: workItem) - } - - /// Checks if a status has already been received by the Push. - /// - /// - parameter status: Status to check - /// - return: True if given status has been received by the Push. - func hasReceived(status: PushStatus) -> Bool { - receivedMessage?.status == status - } - - /// Triggers an event to be sent though the Channel - func trigger(_ status: PushStatus, payload: Payload) { - /// If there is no ref event, then there is nothing to trigger on the channel - guard let refEvent else { return } - - var mutPayload = payload - mutPayload["status"] = status.rawValue - - channel?.trigger(event: refEvent, payload: mutPayload) - } -} diff --git a/Sources/Realtime/Deprecated/RealtimeChannel.swift b/Sources/Realtime/Deprecated/RealtimeChannel.swift deleted file mode 100644 index 22169bc19..000000000 --- a/Sources/Realtime/Deprecated/RealtimeChannel.swift +++ /dev/null @@ -1,1037 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import ConcurrencyExtras -import Foundation -import Swift -import HTTPTypes - -/// Container class of bindings to the channel -struct Binding { - let type: String - let filter: [String: String] - - // The callback to be triggered - let callback: Delegated - - let id: String? -} - -public struct ChannelFilter { - public var event: String? - public var schema: String? - public let table: String? - public let filter: String? - - public init( - event: String? = nil, schema: String? = nil, table: String? = nil, filter: String? = nil - ) { - self.event = event - self.schema = schema - self.table = table - self.filter = filter - } - - var asDictionary: [String: String] { - [ - "event": event, - "schema": schema, - "table": table, - "filter": filter, - ].compactMapValues { $0 } - } -} - -public enum ChannelResponse { - case ok, timedOut, error -} - -public enum RealtimeListenTypes: String { - case postgresChanges = "postgres_changes" - case broadcast - case presence -} - -/// Represents the broadcast and presence options for a channel. -public struct RealtimeChannelOptions { - /// Used to track presence payload across clients. Must be unique per client. If `nil`, the server - /// will generate one. - var presenceKey: String? - /// Enables the client to receive their own`broadcast` messages - var broadcastSelf: Bool - /// Instructs the server to acknowledge the client's `broadcast` messages - var broadcastAcknowledge: Bool - - public init( - presenceKey: String? = nil, - broadcastSelf: Bool = false, - broadcastAcknowledge: Bool = false - ) { - self.presenceKey = presenceKey - self.broadcastSelf = broadcastSelf - self.broadcastAcknowledge = broadcastAcknowledge - } - - /// Parameters used to configure the channel - var params: [String: [String: Any]] { - [ - "config": [ - "presence": [ - "key": presenceKey ?? "", - ], - "broadcast": [ - "ack": broadcastAcknowledge, - "self": broadcastSelf, - ], - ], - ] - } -} - -public enum RealtimeSubscribeStates { - case subscribed - case timedOut - case closed - case channelError -} - -/// -/// Represents a RealtimeChannel which is bound to a topic -/// -/// A RealtimeChannel can bind to multiple events on a given topic and -/// be informed when those events occur within a topic. -/// -/// ### Example: -/// -/// let channel = socket.channel("room:123", params: ["token": "Room Token"]) -/// channel.on("new_msg") { payload in print("Got message", payload") } -/// channel.push("new_msg, payload: ["body": "This is a message"]) -/// .receive("ok") { payload in print("Sent message", payload) } -/// .receive("error") { payload in print("Send failed", payload) } -/// .receive("timeout") { payload in print("Networking issue...", payload) } -/// -/// channel.join() -/// .receive("ok") { payload in print("RealtimeChannel Joined", payload) } -/// .receive("error") { payload in print("Failed ot join", payload) } -/// .receive("timeout") { payload in print("Networking issue...", payload) } -/// -@available( - *, - deprecated, - message: "Use new RealtimeChannelV2 class instead. See migration guide: https://github.com/supabase-community/supabase-swift/blob/main/docs/migrations/RealtimeV2%20Migration%20Guide.md" -) -public class RealtimeChannel { - /// The topic of the RealtimeChannel. e.g. "rooms:friends" - public let topic: String - - /// The params sent when joining the channel - public var params: Payload { - didSet { joinPush.payload = params } - } - - public private(set) lazy var presence = Presence(channel: self) - - /// The Socket that the channel belongs to - weak var socket: RealtimeClient? - - var subTopic: String - - /// Current state of the RealtimeChannel - var state: ChannelState - - /// Collection of event bindings - let bindings: LockIsolated<[String: [Binding]]> - - /// Timeout when attempting to join a RealtimeChannel - var timeout: TimeInterval - - /// Set to true once the channel calls .join() - var joinedOnce: Bool - - /// Push to send when the channel calls .join() - var joinPush: Push! - - /// Buffer of Pushes that will be sent once the RealtimeChannel's socket connects - var pushBuffer: [Push] - - /// Timer to attempt to rejoin - var rejoinTimer: TimeoutTimer - - /// Refs of stateChange hooks - var stateChangeRefs: [String] - - /// Initialize a RealtimeChannel - /// - /// - parameter topic: Topic of the RealtimeChannel - /// - parameter params: Optional. Parameters to send when joining. - /// - parameter socket: Socket that the channel is a part of - init(topic: String, params: [String: Any] = [:], socket: RealtimeClient) { - state = ChannelState.closed - self.topic = topic - subTopic = topic.replacingOccurrences(of: "realtime:", with: "") - self.params = params - self.socket = socket - bindings = LockIsolated([:]) - timeout = socket.timeout - joinedOnce = false - pushBuffer = [] - stateChangeRefs = [] - rejoinTimer = TimeoutTimer() - - // Setup Timer delgation - rejoinTimer.callback - .delegate(to: self) { (self) in - if self.socket?.isConnected == true { self.rejoin() } - } - - rejoinTimer.timerCalculation - .delegate(to: self) { (self, tries) -> TimeInterval in - self.socket?.rejoinAfter(tries) ?? 5.0 - } - - // Respond to socket events - let onErrorRef = self.socket?.delegateOnError( - to: self, - callback: { (self, _) in - self.rejoinTimer.reset() - } - ) - if let ref = onErrorRef { stateChangeRefs.append(ref) } - - let onOpenRef = self.socket?.delegateOnOpen( - to: self, - callback: { (self) in - self.rejoinTimer.reset() - if self.isErrored { self.rejoin() } - } - ) - if let ref = onOpenRef { stateChangeRefs.append(ref) } - - // Setup Push Event to be sent when joining - joinPush = Push( - channel: self, - event: ChannelEvent.join, - payload: self.params, - timeout: timeout - ) - - /// Handle when a response is received after join() - joinPush.delegateReceive(.ok, to: self) { (self, _) in - // Mark the RealtimeChannel as joined - self.state = ChannelState.joined - - // Reset the timer, preventing it from attempting to join again - self.rejoinTimer.reset() - - // Send and buffered messages and clear the buffer - self.pushBuffer.forEach { $0.send() } - self.pushBuffer = [] - } - - // Perform if RealtimeChannel errors while attempting to joi - joinPush.delegateReceive(.error, to: self) { (self, _) in - self.state = .errored - if self.socket?.isConnected == true { self.rejoinTimer.scheduleTimeout() } - } - - // Handle when the join push times out when sending after join() - joinPush.delegateReceive(.timeout, to: self) { (self, _) in - // log that the channel timed out - self.socket?.logItems( - "channel", "timeout \(self.topic) \(self.joinRef ?? "") after \(self.timeout)s" - ) - - // Send a Push to the server to leave the channel - let leavePush = Push( - channel: self, - event: ChannelEvent.leave, - timeout: self.timeout - ) - leavePush.send() - - // Mark the RealtimeChannel as in an error and attempt to rejoin if socket is connected - self.state = ChannelState.errored - self.joinPush.reset() - - if self.socket?.isConnected == true { self.rejoinTimer.scheduleTimeout() } - } - - /// Perfom when the RealtimeChannel has been closed - delegateOnClose(to: self) { (self, _) in - // Reset any timer that may be on-going - self.rejoinTimer.reset() - - // Log that the channel was left - self.socket?.logItems( - "channel", "close topic: \(self.topic) joinRef: \(self.joinRef ?? "nil")" - ) - - // Mark the channel as closed and remove it from the socket - self.state = ChannelState.closed - self.socket?.remove(self) - } - - /// Perfom when the RealtimeChannel errors - delegateOnError(to: self) { (self, message) in - // Log that the channel received an error - self.socket?.logItems( - "channel", "error topic: \(self.topic) joinRef: \(self.joinRef ?? "nil") mesage: \(message)" - ) - - // If error was received while joining, then reset the Push - if self.isJoining { - // Make sure that the "phx_join" isn't buffered to send once the socket - // reconnects. The channel will send a new join event when the socket connects. - if let safeJoinRef = self.joinRef { - self.socket?.removeFromSendBuffer(ref: safeJoinRef) - } - - // Reset the push to be used again later - self.joinPush.reset() - } - - // Mark the channel as errored and attempt to rejoin if socket is currently connected - self.state = ChannelState.errored - if self.socket?.isConnected == true { self.rejoinTimer.scheduleTimeout() } - } - - // Perform when the join reply is received - delegateOn(ChannelEvent.reply, filter: ChannelFilter(), to: self) { (self, message) in - // Trigger bindings - self.trigger( - event: self.replyEventName(message.ref), - payload: message.rawPayload, - ref: message.ref, - joinRef: message.joinRef - ) - } - } - - deinit { - rejoinTimer.reset() - } - - /// Overridable message hook. Receives all events for specialized message - /// handling before dispatching to the channel callbacks. - /// - /// - parameter msg: The Message received by the client from the server - /// - return: Must return the message, modified or unmodified - public var onMessage: (_ message: RealtimeMessage) -> RealtimeMessage = { message in - message - } - - /// Joins the channel - /// - /// - parameter timeout: Optional. Defaults to RealtimeChannel's timeout - /// - return: Push event - @discardableResult - public func subscribe( - timeout: TimeInterval? = nil, - callback: ((RealtimeSubscribeStates, (any Error)?) -> Void)? = nil - ) -> RealtimeChannel { - if socket?.isConnected == false { - socket?.connect() - } - - guard !joinedOnce else { - fatalError( - "tried to join multiple times. 'join' " - + "can only be called a single time per channel instance" - ) - } - - onError { message in - let values = message.payload.values.map { "\($0) " } - let error = RealtimeError(values.isEmpty ? "error" : values.joined(separator: ", ")) - callback?(.channelError, error) - } - - onClose { _ in - callback?(.closed, nil) - } - - // Join the RealtimeChannel - if let safeTimeout = timeout { - self.timeout = safeTimeout - } - - let broadcast = params["config", as: [String: Any].self]?["broadcast"] - let presence = params["config", as: [String: Any].self]?["presence"] - - var accessTokenPayload: Payload = [:] - var config: Payload = [ - "postgres_changes": bindings.value["postgres_changes"]?.map(\.filter) ?? [], - ] - - config["broadcast"] = broadcast - config["presence"] = presence - - if let accessToken = socket?.accessToken { - accessTokenPayload["access_token"] = accessToken - } - - params["config"] = config - - joinedOnce = true - rejoin() - - joinPush - .delegateReceive(.ok, to: self) { (self, message) in - if self.socket?.accessToken != nil { - self.socket?.setAuth(self.socket?.accessToken) - } - - guard let serverPostgresFilters = message.payload["postgres_changes"] as? [[String: Any]] - else { - callback?(.subscribed, nil) - return - } - - let clientPostgresBindings = self.bindings.value["postgres_changes"] ?? [] - let bindingsCount = clientPostgresBindings.count - var newPostgresBindings: [Binding] = [] - - for i in 0 ..< bindingsCount { - let clientPostgresBinding = clientPostgresBindings[i] - - let event = clientPostgresBinding.filter["event"] - let schema = clientPostgresBinding.filter["schema"] - let table = clientPostgresBinding.filter["table"] - let filter = clientPostgresBinding.filter["filter"] - - let serverPostgresFilter = serverPostgresFilters[i] - - if serverPostgresFilter["event", as: String.self] == event, - serverPostgresFilter["schema", as: String.self] == schema, - serverPostgresFilter["table", as: String.self] == table, - serverPostgresFilter["filter", as: String.self] == filter - { - newPostgresBindings.append( - Binding( - type: clientPostgresBinding.type, - filter: clientPostgresBinding.filter, - callback: clientPostgresBinding.callback, - id: serverPostgresFilter["id", as: Int.self].flatMap(String.init) - ) - ) - } else { - self.unsubscribe() - callback?( - .channelError, - RealtimeError("Mismatch between client and server bindings for postgres changes.") - ) - return - } - } - - self.bindings.withValue { [newPostgresBindings] in - $0["postgres_changes"] = newPostgresBindings - } - callback?(.subscribed, nil) - } - .delegateReceive(.error, to: self) { _, message in - let values = message.payload.values.map { "\($0) " } - let error = RealtimeError(values.isEmpty ? "error" : values.joined(separator: ", ")) - callback?(.channelError, error) - } - .delegateReceive(.timeout, to: self) { _, _ in - callback?(.timedOut, nil) - } - - return self - } - - public func presenceState() -> Presence.State { - presence.state - } - - public func track(_ payload: Payload, opts: Payload = [:]) async -> ChannelResponse { - await send( - type: .presence, - payload: [ - "event": "track", - "payload": payload, - ], - opts: opts - ) - } - - public func untrack(opts: Payload = [:]) async -> ChannelResponse { - await send( - type: .presence, - payload: ["event": "untrack"], - opts: opts - ) - } - - /// Hook into when the RealtimeChannel is closed. Does not handle retain cycles. - /// Use `delegateOnClose(to:)` for automatic handling of retain cycles. - /// - /// Example: - /// - /// let channel = socket.channel("topic") - /// channel.onClose() { [weak self] message in - /// self?.print("RealtimeChannel \(message.topic) has closed" - /// } - /// - /// - parameter handler: Called when the RealtimeChannel closes - /// - return: Ref counter of the subscription. See `func off()` - @discardableResult - public func onClose(_ handler: @escaping ((RealtimeMessage) -> Void)) -> RealtimeChannel { - on(ChannelEvent.close, filter: ChannelFilter(), handler: handler) - } - - /// Hook into when the RealtimeChannel is closed. Automatically handles retain - /// cycles. Use `onClose()` to handle yourself. - /// - /// Example: - /// - /// let channel = socket.channel("topic") - /// channel.delegateOnClose(to: self) { (self, message) in - /// self.print("RealtimeChannel \(message.topic) has closed" - /// } - /// - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called when the RealtimeChannel closes - /// - return: Ref counter of the subscription. See `func off()` - @discardableResult - public func delegateOnClose( - to owner: Target, - callback: @escaping ((Target, RealtimeMessage) -> Void) - ) -> RealtimeChannel { - delegateOn( - ChannelEvent.close, filter: ChannelFilter(), to: owner, callback: callback - ) - } - - /// Hook into when the RealtimeChannel receives an Error. Does not handle retain - /// cycles. Use `delegateOnError(to:)` for automatic handling of retain - /// cycles. - /// - /// Example: - /// - /// let channel = socket.channel("topic") - /// channel.onError() { [weak self] (message) in - /// self?.print("RealtimeChannel \(message.topic) has errored" - /// } - /// - /// - parameter handler: Called when the RealtimeChannel closes - /// - return: Ref counter of the subscription. See `func off()` - @discardableResult - public func onError(_ handler: @escaping ((_ message: RealtimeMessage) -> Void)) - -> RealtimeChannel - { - on(ChannelEvent.error, filter: ChannelFilter(), handler: handler) - } - - /// Hook into when the RealtimeChannel receives an Error. Automatically handles - /// retain cycles. Use `onError()` to handle yourself. - /// - /// Example: - /// - /// let channel = socket.channel("topic") - /// channel.delegateOnError(to: self) { (self, message) in - /// self.print("RealtimeChannel \(message.topic) has closed" - /// } - /// - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called when the RealtimeChannel closes - /// - return: Ref counter of the subscription. See `func off()` - @discardableResult - public func delegateOnError( - to owner: Target, - callback: @escaping ((Target, RealtimeMessage) -> Void) - ) -> RealtimeChannel { - delegateOn( - ChannelEvent.error, filter: ChannelFilter(), to: owner, callback: callback - ) - } - - /// Subscribes on channel events. Does not handle retain cycles. Use - /// `delegateOn(_:, to:)` for automatic handling of retain cycles. - /// - /// Subscription returns a ref counter, which can be used later to - /// unsubscribe the exact event listener - /// - /// Example: - /// - /// let channel = socket.channel("topic") - /// let ref1 = channel.on("event") { [weak self] (message) in - /// self?.print("do stuff") - /// } - /// let ref2 = channel.on("event") { [weak self] (message) in - /// self?.print("do other stuff") - /// } - /// channel.off("event", ref1) - /// - /// Since unsubscription of ref1, "do stuff" won't print, but "do other - /// stuff" will keep on printing on the "event" - /// - /// - parameter event: Event to receive - /// - parameter handler: Called with the event's message - /// - return: Ref counter of the subscription. See `func off()` - @discardableResult - public func on( - _ event: String, - filter: ChannelFilter, - handler: @escaping ((RealtimeMessage) -> Void) - ) -> RealtimeChannel { - var delegated = Delegated() - delegated.manuallyDelegate(with: handler) - - return on(event, filter: filter, delegated: delegated) - } - - /// Subscribes on channel events. Automatically handles retain cycles. Use - /// `on()` to handle yourself. - /// - /// Subscription returns a ref counter, which can be used later to - /// unsubscribe the exact event listener - /// - /// Example: - /// - /// let channel = socket.channel("topic") - /// let ref1 = channel.delegateOn("event", to: self) { (self, message) in - /// self?.print("do stuff") - /// } - /// let ref2 = channel.delegateOn("event", to: self) { (self, message) in - /// self?.print("do other stuff") - /// } - /// channel.off("event", ref1) - /// - /// Since unsubscription of ref1, "do stuff" won't print, but "do other - /// stuff" will keep on printing on the "event" - /// - /// - parameter event: Event to receive - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called with the event's message - /// - return: Ref counter of the subscription. See `func off()` - @discardableResult - public func delegateOn( - _ event: String, - filter: ChannelFilter, - to owner: Target, - callback: @escaping ((Target, RealtimeMessage) -> Void) - ) -> RealtimeChannel { - var delegated = Delegated() - delegated.delegate(to: owner, with: callback) - - return on(event, filter: filter, delegated: delegated) - } - - /// Shared method between `on` and `manualOn` - @discardableResult - private func on( - _ type: String, filter: ChannelFilter, delegated: Delegated - ) -> RealtimeChannel { - bindings.withValue { - $0[type.lowercased(), default: []].append( - Binding(type: type.lowercased(), filter: filter.asDictionary, callback: delegated, id: nil) - ) - } - - return self - } - - /// Unsubscribes from a channel event. If a `ref` is given, only the exact - /// listener will be removed. Else all listeners for the `event` will be - /// removed. - /// - /// Example: - /// - /// let channel = socket.channel("topic") - /// let ref1 = channel.on("event") { _ in print("ref1 event" } - /// let ref2 = channel.on("event") { _ in print("ref2 event" } - /// let ref3 = channel.on("other_event") { _ in print("ref3 other" } - /// let ref4 = channel.on("other_event") { _ in print("ref4 other" } - /// channel.off("event", ref1) - /// channel.off("other_event") - /// - /// After this, only "ref2 event" will be printed if the channel receives - /// "event" and nothing is printed if the channel receives "other_event". - /// - /// - parameter event: Event to unsubscribe from - /// - parameter ref: Ref counter returned when subscribing. Can be omitted - public func off(_ type: String, filter: [String: String] = [:]) { - bindings.withValue { - $0[type.lowercased()] = $0[type.lowercased(), default: []].filter { bind in - !(bind.type.lowercased() == type.lowercased() && bind.filter == filter) - } - } - } - - /// Push a payload to the RealtimeChannel - /// - /// Example: - /// - /// channel - /// .push("event", payload: ["message": "hello") - /// .receive("ok") { _ in { print("message sent") } - /// - /// - parameter event: Event to push - /// - parameter payload: Payload to push - /// - parameter timeout: Optional timeout - @discardableResult - public func push( - _ event: String, - payload: Payload, - timeout: TimeInterval = Defaults.timeoutInterval - ) -> Push { - guard joinedOnce else { - fatalError( - "Tried to push \(event) to \(topic) before joining. Use channel.join() before pushing events" - ) - } - - let pushEvent = Push( - channel: self, - event: event, - payload: payload, - timeout: timeout - ) - if canPush { - pushEvent.send() - } else { - pushEvent.startTimeout() - pushBuffer.append(pushEvent) - } - - return pushEvent - } - - public func send( - type: RealtimeListenTypes, - event: String? = nil, - payload: Payload, - opts: Payload = [:] - ) async -> ChannelResponse { - var payload = payload - payload["type"] = type.rawValue - if let event { - payload["event"] = event - } - - if !canPush, type == .broadcast { - var headers = socket?.headers ?? [:] - headers["Content-Type"] = "application/json" - headers["apikey"] = socket?.accessToken - - let body = [ - "messages": [ - "topic": subTopic, - "payload": payload, - "event": event as Any, - ], - ] - - do { - let request = try HTTPRequest( - url: broadcastEndpointURL, - method: .post, - headers: HTTPFields(headers.compactMapValues { $0 }), - body: JSONSerialization.data(withJSONObject: body) - ) - - let response = try await socket?.http.send(request) - guard let response, 200 ..< 300 ~= response.statusCode else { - return .error - } - return .ok - } catch { - return .error - } - } else { - return await withCheckedContinuation { continuation in - let push = self.push( - type.rawValue, payload: payload, - timeout: (opts["timeout"] as? TimeInterval) ?? self.timeout - ) - - if let type = payload["type"] as? String, type == "broadcast", - let config = self.params["config"] as? [String: Any], - let broadcast = config["broadcast"] as? [String: Any] - { - let ack = broadcast["ack"] as? Bool - if ack == nil || ack == false { - continuation.resume(returning: .ok) - return - } - } - - push - .receive(.ok) { _ in - continuation.resume(returning: .ok) - } - .receive(.timeout) { _ in - continuation.resume(returning: .timedOut) - } - } - } - } - - /// Leaves the channel - /// - /// Unsubscribes from server events, and instructs channel to terminate on - /// server - /// - /// Triggers onClose() hooks - /// - /// To receive leave acknowledgements, use the a `receive` - /// hook to bind to the server ack, ie: - /// - /// Example: - //// - /// channel.leave().receive("ok") { _ in { print("left") } - /// - /// - parameter timeout: Optional timeout - /// - return: Push that can add receive hooks - @discardableResult - public func unsubscribe(timeout: TimeInterval = Defaults.timeoutInterval) -> Push { - // If attempting a rejoin during a leave, then reset, cancelling the rejoin - rejoinTimer.reset() - - // Now set the state to leaving - state = .leaving - - /// Delegated callback for a successful or a failed channel leave - var onCloseDelegate = Delegated() - onCloseDelegate.delegate(to: self) { (self, _) in - self.socket?.logItems("channel", "leave \(self.topic)") - - // Triggers onClose() hooks - self.trigger(event: ChannelEvent.close, payload: ["reason": "leave"]) - } - - // Push event to send to the server - let leavePush = Push( - channel: self, - event: ChannelEvent.leave, - timeout: timeout - ) - - // Perform the same behavior if successfully left the channel - // or if sending the event timed out - leavePush - .receive(.ok, delegated: onCloseDelegate) - .receive(.timeout, delegated: onCloseDelegate) - leavePush.send() - - // If the RealtimeChannel cannot send push events, trigger a success locally - if !canPush { - leavePush.trigger(.ok, payload: [:]) - } - - // Return the push so it can be bound to - return leavePush - } - - /// Overridable message hook. Receives all events for specialized message - /// handling before dispatching to the channel callbacks. - /// - /// - parameter event: The event the message was for - /// - parameter payload: The payload for the message - /// - parameter ref: The reference of the message - /// - return: Must return the payload, modified or unmodified - public func onMessage(callback: @escaping (RealtimeMessage) -> RealtimeMessage) { - onMessage = callback - } - - // ---------------------------------------------------------------------- - - // MARK: - Internal - - // ---------------------------------------------------------------------- - /// Checks if an event received by the Socket belongs to this RealtimeChannel - func isMember(_ message: RealtimeMessage) -> Bool { - // Return false if the message's topic does not match the RealtimeChannel's topic - guard message.topic == topic else { return false } - - guard - let safeJoinRef = message.joinRef, - safeJoinRef != joinRef, - ChannelEvent.isLifecyleEvent(message.event) - else { return true } - - socket?.logItems( - "channel", "dropping outdated message", message.topic, message.event, message.rawPayload, - safeJoinRef - ) - return false - } - - /// Sends the payload to join the RealtimeChannel - func sendJoin(_ timeout: TimeInterval) { - state = ChannelState.joining - joinPush.resend(timeout) - } - - /// Rejoins the channel - func rejoin(_ timeout: TimeInterval? = nil) { - // Do not attempt to rejoin if the channel is in the process of leaving - guard !isLeaving else { return } - - // Leave potentially duplicate channels - socket?.leaveOpenTopic(topic: topic) - - // Send the joinPush - sendJoin(timeout ?? self.timeout) - } - - /// Triggers an event to the correct event bindings created by - /// `channel.on("event")`. - /// - /// - parameter message: Message to pass to the event bindings - func trigger(_ message: RealtimeMessage) { - let typeLower = message.event.lowercased() - - let events = Set([ - ChannelEvent.close, - ChannelEvent.error, - ChannelEvent.leave, - ChannelEvent.join, - ]) - - if message.ref != message.joinRef, events.contains(typeLower) { - return - } - - let handledMessage = message - - let bindings: [Binding] = if ["insert", "update", "delete"].contains(typeLower) { - self.bindings.value["postgres_changes", default: []].filter { bind in - bind.filter["event"] == "*" || bind.filter["event"] == typeLower - } - } else { - self.bindings.value[typeLower, default: []].filter { bind in - if ["broadcast", "presence", "postgres_changes"].contains(typeLower) { - let bindEvent = bind.filter["event"]?.lowercased() - - if let bindId = bind.id.flatMap(Int.init) { - let ids = message.payload["ids", as: [Int].self] ?? [] - return ids.contains(bindId) - && ( - bindEvent == "*" - || bindEvent - == message.payload["data", as: [String: Any].self]?["type", as: String.self]? - .lowercased() - ) - } - - return bindEvent == "*" - || bindEvent == message.payload["event", as: String.self]?.lowercased() - } - - return bind.type.lowercased() == typeLower - } - } - - bindings.forEach { $0.callback.call(handledMessage) } - } - - /// Triggers an event to the correct event bindings created by - //// `channel.on("event")`. - /// - /// - parameter event: Event to trigger - /// - parameter payload: Payload of the event - /// - parameter ref: Ref of the event. Defaults to empty - /// - parameter joinRef: Ref of the join event. Defaults to nil - func trigger( - event: String, - payload: Payload = [:], - ref: String = "", - joinRef: String? = nil - ) { - let message = RealtimeMessage( - ref: ref, - topic: topic, - event: event, - payload: payload, - joinRef: joinRef ?? self.joinRef - ) - trigger(message) - } - - /// - parameter ref: The ref of the event push - /// - return: The event name of the reply - func replyEventName(_ ref: String) -> String { - "chan_reply_\(ref)" - } - - /// The Ref send during the join message. - var joinRef: String? { - joinPush.ref - } - - /// - return: True if the RealtimeChannel can push messages, meaning the socket - /// is connected and the channel is joined - var canPush: Bool { - socket?.isConnected == true && isJoined - } - - var broadcastEndpointURL: URL { - var url = socket?.endPoint ?? "" - url = url.replacingOccurrences(of: "^ws", with: "http", options: .regularExpression, range: nil) - url = url.replacingOccurrences( - of: "(/socket/websocket|/socket|/websocket)/?$", with: "", options: .regularExpression, - range: nil - ) - url = - "\(url.replacingOccurrences(of: "/+$", with: "", options: .regularExpression, range: nil))/api/broadcast" - return URL(string: url)! - } -} - -// ---------------------------------------------------------------------- - -// MARK: - Public API - -// ---------------------------------------------------------------------- -extension RealtimeChannel { - /// - return: True if the RealtimeChannel has been closed - public var isClosed: Bool { - state == .closed - } - - /// - return: True if the RealtimeChannel experienced an error - public var isErrored: Bool { - state == .errored - } - - /// - return: True if the channel has joined - public var isJoined: Bool { - state == .joined - } - - /// - return: True if the channel has requested to join - public var isJoining: Bool { - state == .joining - } - - /// - return: True if the channel has requested to leave - public var isLeaving: Bool { - state == .leaving - } -} - -extension [String: Any] { - subscript(_ key: Key, as _: T.Type) -> T? { - self[key] as? T - } -} diff --git a/Sources/Realtime/Deprecated/RealtimeClient.swift b/Sources/Realtime/Deprecated/RealtimeClient.swift deleted file mode 100644 index d1eabe92f..000000000 --- a/Sources/Realtime/Deprecated/RealtimeClient.swift +++ /dev/null @@ -1,1071 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import ConcurrencyExtras -import Foundation - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -public enum SocketError: Error { - case abnormalClosureError -} - -/// Alias for a JSON dictionary [String: Any] -public typealias Payload = [String: Any] - -/// Alias for a function returning an optional JSON dictionary (`Payload?`) -public typealias PayloadClosure = () -> Payload? - -/// Struct that gathers callbacks assigned to the Socket -struct StateChangeCallbacks { - var open: LockIsolated<[(ref: String, callback: Delegated)]> = .init([]) - var close: LockIsolated<[(ref: String, callback: Delegated<(Int, String?), Void>)]> = .init([]) - var error: LockIsolated<[(ref: String, callback: Delegated<(any Error, URLResponse?), Void>)]> = - .init([]) - var message: LockIsolated<[(ref: String, callback: Delegated)]> = .init([]) -} - -/// ## Socket Connection -/// A single connection is established to the server and -/// channels are multiplexed over the connection. -/// Connect to the server using the `RealtimeClient` class: -/// -/// ```swift -/// let socket = new RealtimeClient("/socket", paramsClosure: { ["userToken": "123" ] }) -/// socket.connect() -/// ``` -/// -/// The `RealtimeClient` constructor takes the mount point of the socket, -/// the authentication params, as well as options that can be found in -/// the Socket docs, such as configuring the heartbeat. -@available( - *, - deprecated, - message: "Use new RealtimeClientV2 class instead. See migration guide: https://github.com/supabase-community/supabase-swift/blob/main/docs/migrations/RealtimeV2%20Migration%20Guide.md" -) -public class RealtimeClient: PhoenixTransportDelegate { - // ---------------------------------------------------------------------- - - // MARK: - Public Attributes - - // ---------------------------------------------------------------------- - /// The string WebSocket endpoint (ie `"ws://example.com/socket"`, - /// `"wss://example.com"`, etc.) That was passed to the Socket during - /// initialization. The URL endpoint will be modified by the Socket to - /// include `"/websocket"` if missing. - public let endPoint: String - - /// The fully qualified socket URL - public private(set) var endPointUrl: URL - - /// Resolves to return the `paramsClosure` result at the time of calling. - /// If the `Socket` was created with static params, then those will be - /// returned every time. - public var params: Payload? { - paramsClosure?() - } - - /// The optional params closure used to get params when connecting. Must - /// be set when initializing the Socket. - public let paramsClosure: PayloadClosure? - - /// The WebSocket transport. Default behavior is to provide a - /// URLSessionWebsocketTask. See README for alternatives. - private let transport: (URL) -> any PhoenixTransport - - /// Phoenix serializer version, defaults to "2.0.0" - public let vsn: String - - /// Override to provide custom encoding of data before writing to the socket - public var encode: (Any) -> Data = Defaults.encode - - /// Override to provide custom decoding of data read from the socket - public var decode: (Data) -> Any? = Defaults.decode - - /// Timeout to use when opening connections - public var timeout: TimeInterval = Defaults.timeoutInterval - - /// Custom headers to be added to the socket connection request - public var headers: [String: String] = [:] - - /// Interval between sending a heartbeat - public var heartbeatInterval: TimeInterval = Defaults.heartbeatInterval - - /// The maximum amount of time which the system may delay heartbeats in order to optimize power - /// usage - public var heartbeatLeeway: DispatchTimeInterval = Defaults.heartbeatLeeway - - /// Interval between socket reconnect attempts, in seconds - public var reconnectAfter: (Int) -> TimeInterval = Defaults.reconnectSteppedBackOff - - /// Interval between channel rejoin attempts, in seconds - public var rejoinAfter: (Int) -> TimeInterval = Defaults.rejoinSteppedBackOff - - /// The optional function to receive logs - public var logger: ((String) -> Void)? - - /// Disables heartbeats from being sent. Default is false. - public var skipHeartbeat: Bool = false - - /// Enable/Disable SSL certificate validation. Default is false. This - /// must be set before calling `socket.connect()` in order to be applied - public var disableSSLCertValidation: Bool = false - - #if os(Linux) || os(Windows) || os(Android) - #else - /// Configure custom SSL validation logic, eg. SSL pinning. This - /// must be set before calling `socket.connect()` in order to apply. - // public var security: SSLTrustValidator? - - /// Configure the encryption used by your client by setting the - /// allowed cipher suites supported by your server. This must be - /// set before calling `socket.connect()` in order to apply. - public var enabledSSLCipherSuites: [SSLCipherSuite]? - #endif - - // ---------------------------------------------------------------------- - - // MARK: - Private Attributes - - // ---------------------------------------------------------------------- - /// Callbacks for socket state changes - var stateChangeCallbacks: StateChangeCallbacks = .init() - - /// Collection on channels created for the Socket - public internal(set) var channels: [RealtimeChannel] = [] - - /// Buffers messages that need to be sent once the socket has connected. It is an array - /// of tuples, with the ref of the message to send and the callback that will send the message. - var sendBuffer: [(ref: String?, callback: () throws -> Void)] = [] - - /// Ref counter for messages - var ref: UInt64 = .min // 0 (max: 18,446,744,073,709,551,615) - - /// Timer that triggers sending new Heartbeat messages - var heartbeatTimer: HeartbeatTimer? - - /// Ref counter for the last heartbeat that was sent - var pendingHeartbeatRef: String? - - /// Timer to use when attempting to reconnect - var reconnectTimer: TimeoutTimer - - /// Close status - var closeStatus: CloseStatus = .unknown - - /// The connection to the server - var connection: (any PhoenixTransport)? = nil - - /// The HTTPClient to perform HTTP requests. - let http: any HTTPClientType - - var accessToken: String? - - // ---------------------------------------------------------------------- - - // MARK: - Initialization - - // ---------------------------------------------------------------------- - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) - public convenience init( - _ endPoint: String, - headers: [String: String] = [:], - params: Payload? = nil, - vsn: String = Defaults.vsn - ) { - self.init( - endPoint: endPoint, - headers: headers, - transport: { url in URLSessionTransport(url: url) }, - paramsClosure: { params }, - vsn: vsn - ) - } - - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) - public convenience init( - _ endPoint: String, - headers: [String: String] = [:], - paramsClosure: PayloadClosure?, - vsn: String = Defaults.vsn - ) { - self.init( - endPoint: endPoint, - headers: headers, - transport: { url in URLSessionTransport(url: url) }, - paramsClosure: paramsClosure, - vsn: vsn - ) - } - - public init( - endPoint: String, - headers: [String: String] = [:], - transport: @escaping ((URL) -> any PhoenixTransport), - paramsClosure: PayloadClosure? = nil, - vsn: String = Defaults.vsn - ) { - self.transport = transport - self.paramsClosure = paramsClosure - self.endPoint = endPoint - self.vsn = vsn - - var headers = headers - if headers["X-Client-Info"] == nil { - headers["X-Client-Info"] = "realtime-swift/\(version)" - } - self.headers = headers - http = HTTPClient(fetch: { try await URLSession.shared.data(for: $0) }, interceptors: []) - - let params = paramsClosure?() - if let jwt = (params?["Authorization"] as? String)?.split(separator: " ").last { - accessToken = String(jwt) - } else { - accessToken = params?["apikey"] as? String - } - endPointUrl = RealtimeClient.buildEndpointUrl( - endpoint: endPoint, - paramsClosure: paramsClosure, - vsn: vsn - ) - - reconnectTimer = TimeoutTimer() - reconnectTimer.callback.delegate(to: self) { (self) in - self.logItems("Socket attempting to reconnect") - self.teardown(reason: "reconnection") { self.connect() } - } - reconnectTimer.timerCalculation - .delegate(to: self) { (self, tries) -> TimeInterval in - let interval = self.reconnectAfter(tries) - self.logItems("Socket reconnecting in \(interval)s") - return interval - } - } - - deinit { - reconnectTimer.reset() - } - - // ---------------------------------------------------------------------- - - // MARK: - Public - - // ---------------------------------------------------------------------- - /// - return: The socket protocol, wss or ws - public var websocketProtocol: String { - switch endPointUrl.scheme { - case "https": "wss" - case "http": "ws" - default: endPointUrl.scheme ?? "" - } - } - - /// - return: True if the socket is connected - public var isConnected: Bool { - connectionState == .open - } - - /// - return: The state of the connect. [.connecting, .open, .closing, .closed] - public var connectionState: PhoenixTransportReadyState { - connection?.readyState ?? .closed - } - - /// Sets the JWT access token used for channel subscription authorization and Realtime RLS. - /// - Parameter token: A JWT string. - public func setAuth(_ token: String?) { - accessToken = token - - for channel in channels { - if token != nil { - channel.params["user_token"] = token - } - - if channel.joinedOnce, channel.isJoined { - channel.push(ChannelEvent.accessToken, payload: ["access_token": token as Any]) - } - } - } - - /// Connects the Socket. The params passed to the Socket on initialization - /// will be sent through the connection. If the Socket is already connected, - /// then this call will be ignored. - public func connect() { - // Do not attempt to reconnect if the socket is currently connected - guard !isConnected else { return } - - // Reset the close status when attempting to connect - closeStatus = .unknown - - // We need to build this right before attempting to connect as the - // parameters could be built upon demand and change over time - endPointUrl = RealtimeClient.buildEndpointUrl( - endpoint: endPoint, - paramsClosure: paramsClosure, - vsn: vsn - ) - - connection = transport(endPointUrl) - connection?.delegate = self - // self.connection?.disableSSLCertValidation = disableSSLCertValidation - // - // #if os(Linux) - // #else - // self.connection?.security = security - // self.connection?.enabledSSLCipherSuites = enabledSSLCipherSuites - // #endif - - connection?.connect(with: headers) - } - - /// Disconnects the socket - /// - /// - parameter code: Optional. Closing status code - /// - parameter callback: Optional. Called when disconnected - public func disconnect( - code: CloseCode = CloseCode.normal, - reason: String? = nil, - callback: (() -> Void)? = nil - ) { - // The socket was closed cleanly by the User - closeStatus = CloseStatus(closeCode: code.rawValue) - - // Reset any reconnects and teardown the socket connection - reconnectTimer.reset() - teardown(code: code, reason: reason, callback: callback) - } - - func teardown( - code: CloseCode = CloseCode.normal, reason: String? = nil, callback: (() -> Void)? = nil - ) { - connection?.delegate = nil - connection?.disconnect(code: code.rawValue, reason: reason) - connection = nil - - // The socket connection has been turndown, heartbeats are not needed - heartbeatTimer?.stop() - - // Since the connection's delegate was nil'd out, inform all state - // callbacks that the connection has closed - stateChangeCallbacks.close.value.forEach { $0.callback.call((code.rawValue, reason)) } - callback?() - } - - // ---------------------------------------------------------------------- - - // MARK: - Register Socket State Callbacks - - // ---------------------------------------------------------------------- - - /// Registers callbacks for connection open events. Does not handle retain - /// cycles. Use `delegateOnOpen(to:)` for automatic handling of retain cycles. - /// - /// Example: - /// - /// socket.onOpen() { [weak self] in - /// self?.print("Socket Connection Open") - /// } - /// - /// - parameter callback: Called when the Socket is opened - @discardableResult - public func onOpen(callback: @escaping () -> Void) -> String { - onOpen { _ in callback() } - } - - /// Registers callbacks for connection open events. Does not handle retain - /// cycles. Use `delegateOnOpen(to:)` for automatic handling of retain cycles. - /// - /// Example: - /// - /// socket.onOpen() { [weak self] response in - /// self?.print("Socket Connection Open") - /// } - /// - /// - parameter callback: Called when the Socket is opened - @discardableResult - public func onOpen(callback: @escaping (URLResponse?) -> Void) -> String { - var delegated = Delegated() - delegated.manuallyDelegate(with: callback) - - return stateChangeCallbacks.open.withValue { [delegated] in - self.append(callback: delegated, to: &$0) - } - } - - /// Registers callbacks for connection open events. Automatically handles - /// retain cycles. Use `onOpen()` to handle yourself. - /// - /// Example: - /// - /// socket.delegateOnOpen(to: self) { self in - /// self.print("Socket Connection Open") - /// } - /// - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called when the Socket is opened - @discardableResult - public func delegateOnOpen( - to owner: T, - callback: @escaping ((T) -> Void) - ) -> String { - delegateOnOpen(to: owner) { owner, _ in callback(owner) } - } - - /// Registers callbacks for connection open events. Automatically handles - /// retain cycles. Use `onOpen()` to handle yourself. - /// - /// Example: - /// - /// socket.delegateOnOpen(to: self) { self, response in - /// self.print("Socket Connection Open") - /// } - /// - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called when the Socket is opened - @discardableResult - public func delegateOnOpen( - to owner: T, - callback: @escaping ((T, URLResponse?) -> Void) - ) -> String { - var delegated = Delegated() - delegated.delegate(to: owner, with: callback) - - return stateChangeCallbacks.open.withValue { [delegated] in - self.append(callback: delegated, to: &$0) - } - } - - /// Registers callbacks for connection close events. Does not handle retain - /// cycles. Use `delegateOnClose(_:)` for automatic handling of retain cycles. - /// - /// Example: - /// - /// socket.onClose() { [weak self] in - /// self?.print("Socket Connection Close") - /// } - /// - /// - parameter callback: Called when the Socket is closed - @discardableResult - public func onClose(callback: @escaping () -> Void) -> String { - onClose { _, _ in callback() } - } - - /// Registers callbacks for connection close events. Does not handle retain - /// cycles. Use `delegateOnClose(_:)` for automatic handling of retain cycles. - /// - /// Example: - /// - /// socket.onClose() { [weak self] code, reason in - /// self?.print("Socket Connection Close") - /// } - /// - /// - parameter callback: Called when the Socket is closed - @discardableResult - public func onClose(callback: @escaping (Int, String?) -> Void) -> String { - var delegated = Delegated<(Int, String?), Void>() - delegated.manuallyDelegate(with: callback) - - return stateChangeCallbacks.close.withValue { [delegated] in - self.append(callback: delegated, to: &$0) - } - } - - /// Registers callbacks for connection close events. Automatically handles - /// retain cycles. Use `onClose()` to handle yourself. - /// - /// Example: - /// - /// socket.delegateOnClose(self) { self in - /// self.print("Socket Connection Close") - /// } - /// - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called when the Socket is closed - @discardableResult - public func delegateOnClose( - to owner: T, - callback: @escaping ((T) -> Void) - ) -> String { - delegateOnClose(to: owner) { owner, _ in callback(owner) } - } - - /// Registers callbacks for connection close events. Automatically handles - /// retain cycles. Use `onClose()` to handle yourself. - /// - /// Example: - /// - /// socket.delegateOnClose(self) { self, code, reason in - /// self.print("Socket Connection Close") - /// } - /// - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called when the Socket is closed - @discardableResult - public func delegateOnClose( - to owner: T, - callback: @escaping ((T, (Int, String?)) -> Void) - ) -> String { - var delegated = Delegated<(Int, String?), Void>() - delegated.delegate(to: owner, with: callback) - - return stateChangeCallbacks.close.withValue { [delegated] in - self.append(callback: delegated, to: &$0) - } - } - - /// Registers callbacks for connection error events. Does not handle retain - /// cycles. Use `delegateOnError(to:)` for automatic handling of retain cycles. - /// - /// Example: - /// - /// socket.onError() { [weak self] (error) in - /// self?.print("Socket Connection Error", error) - /// } - /// - /// - parameter callback: Called when the Socket errors - @discardableResult - public func onError(callback: @escaping ((any Error, URLResponse?)) -> Void) -> String { - var delegated = Delegated<(any Error, URLResponse?), Void>() - delegated.manuallyDelegate(with: callback) - - return stateChangeCallbacks.error.withValue { [delegated] in - self.append(callback: delegated, to: &$0) - } - } - - /// Registers callbacks for connection error events. Automatically handles - /// retain cycles. Use `manualOnError()` to handle yourself. - /// - /// Example: - /// - /// socket.delegateOnError(to: self) { (self, error) in - /// self.print("Socket Connection Error", error) - /// } - /// - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called when the Socket errors - @discardableResult - public func delegateOnError( - to owner: T, - callback: @escaping ((T, (any Error, URLResponse?)) -> Void) - ) -> String { - var delegated = Delegated<(any Error, URLResponse?), Void>() - delegated.delegate(to: owner, with: callback) - - return stateChangeCallbacks.error.withValue { [delegated] in - self.append(callback: delegated, to: &$0) - } - } - - /// Registers callbacks for connection message events. Does not handle - /// retain cycles. Use `delegateOnMessage(_to:)` for automatic handling of - /// retain cycles. - /// - /// Example: - /// - /// socket.onMessage() { [weak self] (message) in - /// self?.print("Socket Connection Message", message) - /// } - /// - /// - parameter callback: Called when the Socket receives a message event - @discardableResult - public func onMessage(callback: @escaping (RealtimeMessage) -> Void) -> String { - var delegated = Delegated() - delegated.manuallyDelegate(with: callback) - - return stateChangeCallbacks.message.withValue { [delegated] in - append(callback: delegated, to: &$0) - } - } - - /// Registers callbacks for connection message events. Automatically handles - /// retain cycles. Use `onMessage()` to handle yourself. - /// - /// Example: - /// - /// socket.delegateOnMessage(self) { (self, message) in - /// self.print("Socket Connection Message", message) - /// } - /// - /// - parameter owner: Class registering the callback. Usually `self` - /// - parameter callback: Called when the Socket receives a message event - @discardableResult - public func delegateOnMessage( - to owner: T, - callback: @escaping ((T, RealtimeMessage) -> Void) - ) -> String { - var delegated = Delegated() - delegated.delegate(to: owner, with: callback) - - return stateChangeCallbacks.message.withValue { [delegated] in - self.append(callback: delegated, to: &$0) - } - } - - private func append(callback: T, to array: inout [(ref: String, callback: T)]) - -> String - { - let ref = makeRef() - array.append((ref, callback)) - return ref - } - - /// Releases all stored callback hooks (onError, onOpen, onClose, etc.) You should - /// call this method when you are finished when the Socket in order to release - /// any references held by the socket. - public func releaseCallbacks() { - stateChangeCallbacks.open.setValue([]) - stateChangeCallbacks.close.setValue([]) - stateChangeCallbacks.error.setValue([]) - stateChangeCallbacks.message.setValue([]) - } - - // ---------------------------------------------------------------------- - - // MARK: - Channel Initialization - - // ---------------------------------------------------------------------- - /// Initialize a new Channel - /// - /// Example: - /// - /// let channel = socket.channel("rooms", params: ["user_id": "abc123"]) - /// - /// - parameter topic: Topic of the channel - /// - parameter params: Optional. Parameters for the channel - /// - return: A new channel - public func channel( - _ topic: String, - params: RealtimeChannelOptions = .init() - ) -> RealtimeChannel { - let channel = RealtimeChannel( - topic: "realtime:\(topic)", params: params.params, socket: self - ) - channels.append(channel) - - return channel - } - - /// Unsubscribes and removes a single channel - public func remove(_ channel: RealtimeChannel) { - channel.unsubscribe() - off(channel.stateChangeRefs) - channels.removeAll(where: { $0.joinRef == channel.joinRef }) - - if channels.isEmpty { - disconnect() - } - } - - /// Unsubscribes and removes all channels - public func removeAllChannels() { - for channel in channels { - remove(channel) - } - } - - /// Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations. - /// - /// - /// - Parameter refs: List of refs returned by calls to `onOpen`, `onClose`, etc - public func off(_ refs: [String]) { - stateChangeCallbacks.open.withValue { - $0 = $0.filter { - !refs.contains($0.ref) - } - } - stateChangeCallbacks.close.withValue { - $0 = $0.filter { - !refs.contains($0.ref) - } - } - stateChangeCallbacks.error.withValue { - $0 = $0.filter { - !refs.contains($0.ref) - } - } - stateChangeCallbacks.message.withValue { - $0 = $0.filter { - !refs.contains($0.ref) - } - } - } - - // ---------------------------------------------------------------------- - - // MARK: - Sending Data - - // ---------------------------------------------------------------------- - /// Sends data through the Socket. This method is internal. Instead, you - /// should call `push(_:, payload:, timeout:)` on the Channel you are - /// sending an event to. - /// - /// - parameter topic: - /// - parameter event: - /// - parameter payload: - /// - parameter ref: Optional. Defaults to nil - /// - parameter joinRef: Optional. Defaults to nil - func push( - topic: String, - event: String, - payload: Payload, - ref: String? = nil, - joinRef: String? = nil - ) { - let callback: (() throws -> Void) = { [weak self] in - guard let self else { return } - let body: [Any?] = [joinRef, ref, topic, event, payload] - let data = encode(body) - - logItems("push", "Sending \(String(data: data, encoding: String.Encoding.utf8) ?? "")") - connection?.send(data: data) - } - - /// If the socket is connected, then execute the callback immediately. - if isConnected { - try? callback() - } else { - /// If the socket is not connected, add the push to a buffer which will - /// be sent immediately upon connection. - sendBuffer.append((ref: ref, callback: callback)) - } - } - - /// - return: the next message ref, accounting for overflows - public func makeRef() -> String { - ref = (ref == UInt64.max) ? 0 : ref + 1 - return String(ref) - } - - /// Logs the message. Override Socket.logger for specialized logging. noops by default - /// - /// - parameter items: List of items to be logged. Behaves just like debugPrint() - func logItems(_ items: Any...) { - let msg = items.map { String(describing: $0) }.joined(separator: ", ") - logger?("SwiftPhoenixClient: \(msg)") - } - - // ---------------------------------------------------------------------- - - // MARK: - Connection Events - - // ---------------------------------------------------------------------- - /// Called when the underlying Websocket connects to it's host - func onConnectionOpen(response: URLResponse?) { - logItems("transport", "Connected to \(endPoint)") - - // Reset the close status now that the socket has been connected - closeStatus = .unknown - - // Send any messages that were waiting for a connection - flushSendBuffer() - - // Reset how the socket tried to reconnect - reconnectTimer.reset() - - // Restart the heartbeat timer - resetHeartbeat() - - // Inform all onOpen callbacks that the Socket has opened - stateChangeCallbacks.open.value.forEach { $0.callback.call(response) } - } - - func onConnectionClosed(code: Int, reason: String?) { - logItems("transport", "close") - - // Send an error to all channels - triggerChannelError() - - // Prevent the heartbeat from triggering if the - heartbeatTimer?.stop() - - // Only attempt to reconnect if the socket did not close normally, - // or if it was closed abnormally but on client side (e.g. due to heartbeat timeout) - if closeStatus.shouldReconnect { - reconnectTimer.scheduleTimeout() - } - - stateChangeCallbacks.close.value.forEach { $0.callback.call((code, reason)) } - } - - func onConnectionError(_ error: any Error, response: URLResponse?) { - logItems("transport", error, response ?? "") - - // Send an error to all channels - triggerChannelError() - - // Inform any state callbacks of the error - stateChangeCallbacks.error.value.forEach { $0.callback.call((error, response)) } - } - - func onConnectionMessage(_ rawMessage: String) { - logItems("receive ", rawMessage) - - guard - let data = rawMessage.data(using: String.Encoding.utf8), - let json = decode(data) as? [Any?], - let message = RealtimeMessage(json: json) - else { - logItems("receive: Unable to parse JSON: \(rawMessage)") - return - } - - // Clear heartbeat ref, preventing a heartbeat timeout disconnect - if message.ref == pendingHeartbeatRef { pendingHeartbeatRef = nil } - - if message.event == "phx_close" { - print("Close Event Received") - } - - // Dispatch the message to all channels that belong to the topic - channels - .filter { $0.isMember(message) } - .forEach { $0.trigger(message) } - - // Inform all onMessage callbacks of the message - stateChangeCallbacks.message.value.forEach { $0.callback.call(message) } - } - - /// Triggers an error event to all of the connected Channels - func triggerChannelError() { - for channel in channels { - // Only trigger a channel error if it is in an "opened" state - if !(channel.isErrored || channel.isLeaving || channel.isClosed) { - channel.trigger(event: ChannelEvent.error) - } - } - } - - /// Send all messages that were buffered before the socket opened - func flushSendBuffer() { - guard isConnected, sendBuffer.count > 0 else { return } - sendBuffer.forEach { try? $0.callback() } - sendBuffer = [] - } - - /// Removes an item from the sendBuffer with the matching ref - func removeFromSendBuffer(ref: String) { - sendBuffer = sendBuffer.filter { $0.ref != ref } - } - - /// Builds a fully qualified socket `URL` from `endPoint` and `params`. - static func buildEndpointUrl( - endpoint: String, paramsClosure params: PayloadClosure?, vsn: String - ) -> URL { - guard - let url = URL(string: endpoint), - var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) - else { fatalError("Malformed URL: \(endpoint)") } - - // Ensure that the URL ends with "/websocket - if !urlComponents.path.contains("/websocket") { - // Do not duplicate '/' in the path - if urlComponents.path.last != "/" { - urlComponents.path.append("/") - } - - // append 'websocket' to the path - urlComponents.path.append("websocket") - } - - urlComponents.queryItems = [URLQueryItem(name: "vsn", value: vsn)] - - // If there are parameters, append them to the URL - if let params = params?() { - urlComponents.queryItems?.append( - contentsOf: params.map { - URLQueryItem(name: $0.key, value: String(describing: $0.value)) - } - ) - } - - guard let qualifiedUrl = urlComponents.url - else { fatalError("Malformed URL while adding parameters") } - return qualifiedUrl - } - - // Leaves any channel that is open that has a duplicate topic - func leaveOpenTopic(topic: String) { - guard - let dupe = channels.first(where: { $0.topic == topic && ($0.isJoined || $0.isJoining) }) - else { return } - - logItems("transport", "leaving duplicate topic: [\(topic)]") - dupe.unsubscribe() - } - - // ---------------------------------------------------------------------- - - // MARK: - Heartbeat - - // ---------------------------------------------------------------------- - func resetHeartbeat() { - // Clear anything related to the heartbeat - pendingHeartbeatRef = nil - heartbeatTimer?.stop() - - // Do not start up the heartbeat timer if skipHeartbeat is true - guard !skipHeartbeat else { return } - - heartbeatTimer = HeartbeatTimer(timeInterval: heartbeatInterval, leeway: heartbeatLeeway) - heartbeatTimer?.start(eventHandler: { [weak self] in - self?.sendHeartbeat() - }) - } - - /// Sends a heartbeat payload to the phoenix servers - func sendHeartbeat() { - // Do not send if the connection is closed - guard isConnected else { return } - - // If there is a pending heartbeat ref, then the last heartbeat was - // never acknowledged by the server. Close the connection and attempt - // to reconnect. - if let _ = pendingHeartbeatRef { - pendingHeartbeatRef = nil - logItems( - "transport", - "heartbeat timeout. Attempting to re-establish connection" - ) - - // Close the socket manually, flagging the closure as abnormal. Do not use - // `teardown` or `disconnect` as they will nil out the websocket delegate. - abnormalClose("heartbeat timeout") - - return - } - - // The last heartbeat was acknowledged by the server. Send another one - pendingHeartbeatRef = makeRef() - push( - topic: "phoenix", - event: ChannelEvent.heartbeat, - payload: [:], - ref: pendingHeartbeatRef - ) - } - - func abnormalClose(_ reason: String) { - closeStatus = .abnormal - - /* - We use NORMAL here since the client is the one determining to close the - connection. However, we set to close status to abnormal so that - the client knows that it should attempt to reconnect. - - If the server subsequently acknowledges with code 1000 (normal close), - the socket will keep the `.abnormal` close status and trigger a reconnection. - */ - connection?.disconnect(code: CloseCode.normal.rawValue, reason: reason) - } - - // ---------------------------------------------------------------------- - - // MARK: - TransportDelegate - - // ---------------------------------------------------------------------- - public func onOpen(response: URLResponse?) { - onConnectionOpen(response: response) - } - - public func onError(error: any Error, response: URLResponse?) { - onConnectionError(error, response: response) - } - - public func onMessage(message: String) { - onConnectionMessage(message) - } - - public func onClose(code: Int, reason: String? = nil) { - closeStatus.update(transportCloseCode: code) - onConnectionClosed(code: code, reason: reason) - } -} - -// ---------------------------------------------------------------------- - -// MARK: - Close Codes - -// ---------------------------------------------------------------------- -extension RealtimeClient { - public enum CloseCode: Int { - case abnormal = 999 - - case normal = 1000 - - case goingAway = 1001 - } -} - -// ---------------------------------------------------------------------- - -// MARK: - Close Status - -// ---------------------------------------------------------------------- -extension RealtimeClient { - /// Indicates the different closure states a socket can be in. - enum CloseStatus { - /// Undetermined closure state - case unknown - /// A clean closure requested either by the client or the server - case clean - /// An abnormal closure requested by the client - case abnormal - - /// Temporarily close the socket, pausing reconnect attempts. Useful on mobile - /// clients when disconnecting a because the app resigned active but should - /// reconnect when app enters active state. - case temporary - - init(closeCode: Int) { - switch closeCode { - case CloseCode.abnormal.rawValue: - self = .abnormal - case CloseCode.goingAway.rawValue: - self = .temporary - default: - self = .clean - } - } - - mutating func update(transportCloseCode: Int) { - switch self { - case .unknown, .clean, .temporary: - // Allow transport layer to override these statuses. - self = .init(closeCode: transportCloseCode) - case .abnormal: - // Do not allow transport layer to override the abnormal close status. - // The socket itself should reset it on the next connection attempt. - // See `Socket.abnormalClose(_:)` for more information. - break - } - } - - var shouldReconnect: Bool { - switch self { - case .unknown, .abnormal: - true - case .clean, .temporary: - false - } - } - } -} diff --git a/Sources/Realtime/Deprecated/RealtimeMessage.swift b/Sources/Realtime/Deprecated/RealtimeMessage.swift deleted file mode 100644 index a993ae2d1..000000000 --- a/Sources/Realtime/Deprecated/RealtimeMessage.swift +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// Data that is received from the Server. -public struct RealtimeMessage { - /// Reference number. Empty if missing - public let ref: String - - /// Join Reference number - let joinRef: String? - - /// Message topic - public let topic: String - - /// Message event - public let event: String - - /// The raw payload from the Message, including a nested response from - /// phx_reply events. It is recommended to use `payload` instead. - let rawPayload: Payload - - /// Message payload - public var payload: Payload { - guard let response = rawPayload["response"] as? Payload - else { return rawPayload } - return response - } - - /// Convenience accessor. Equivalent to getting the status as such: - /// ```swift - /// message.payload["status"] - /// ``` - public var status: PushStatus? { - (rawPayload["status"] as? String).flatMap(PushStatus.init(rawValue:)) - } - - init( - ref: String = "", - topic: String = "", - event: String = "", - payload: Payload = [:], - joinRef: String? = nil - ) { - self.ref = ref - self.topic = topic - self.event = event - rawPayload = payload - self.joinRef = joinRef - } - - init?(json: [Any?]) { - guard json.count > 4 else { return nil } - joinRef = json[0] as? String - ref = json[1] as? String ?? "" - - if let topic = json[2] as? String, - let event = json[3] as? String, - let payload = json[4] as? Payload - { - self.topic = topic - self.event = event - rawPayload = payload - } else { - return nil - } - } -} diff --git a/Sources/Realtime/Deprecated/TimeoutTimer.swift b/Sources/Realtime/Deprecated/TimeoutTimer.swift deleted file mode 100644 index b6b37c4c7..000000000 --- a/Sources/Realtime/Deprecated/TimeoutTimer.swift +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2021 David Stump -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -/// Creates a timer that can perform calculated reties by setting -/// `timerCalculation` , such as exponential backoff. -/// -/// ### Example -/// -/// let reconnectTimer = TimeoutTimer() -/// -/// // Receive a callbcak when the timer is fired -/// reconnectTimer.callback.delegate(to: self) { (_) in -/// print("timer was fired") -/// } -/// -/// // Provide timer interval calculation -/// reconnectTimer.timerCalculation.delegate(to: self) { (_, tries) -> TimeInterval in -/// return tries > 2 ? 1000 : [1000, 5000, 10000][tries - 1] -/// } -/// -/// reconnectTimer.scheduleTimeout() // fires after 1000ms -/// reconnectTimer.scheduleTimeout() // fires after 5000ms -/// reconnectTimer.reset() -/// reconnectTimer.scheduleTimeout() // fires after 1000ms - -import Foundation - -// sourcery: AutoMockable -class TimeoutTimer { - /// Callback to be informed when the underlying Timer fires - var callback = Delegated() - - /// Provides TimeInterval to use when scheduling the timer - var timerCalculation = Delegated() - - /// The work to be done when the queue fires - var workItem: DispatchWorkItem? - - /// The number of times the underlyingTimer hass been set off. - var tries: Int = 0 - - /// The Queue to execute on. In testing, this is overridden - var queue: TimerQueue = .main - - /// Resets the Timer, clearing the number of tries and stops - /// any scheduled timeout. - func reset() { - tries = 0 - clearTimer() - } - - /// Schedules a timeout callback to fire after a calculated timeout duration. - func scheduleTimeout() { - // Clear any ongoing timer, not resetting the number of tries - clearTimer() - - // Get the next calculated interval, in milliseconds. Do not - // start the timer if the interval is returned as nil. - guard let timeInterval = timerCalculation.call(tries + 1) else { return } - - let workItem = DispatchWorkItem { - self.tries += 1 - self.callback.call() - } - - self.workItem = workItem - queue.queue(timeInterval: timeInterval, execute: workItem) - } - - /// Invalidates any ongoing Timer. Will not clear how many tries have been made - private func clearTimer() { - workItem?.cancel() - workItem = nil - } -} - -/// Wrapper class around a DispatchQueue. Allows for providing a fake clock -/// during tests. -class TimerQueue { - // Can be overriden in tests - static var main = TimerQueue() - - func queue(timeInterval: TimeInterval, execute: DispatchWorkItem) { - // TimeInterval is always in seconds. Multiply it by 1000 to convert - // to milliseconds and round to the nearest millisecond. - let dispatchInterval = Int(round(timeInterval * 1000)) - - let dispatchTime = DispatchTime.now() + .milliseconds(dispatchInterval) - DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: execute) - } -} diff --git a/Sources/Realtime/RealtimeChannel+AsyncAwait.swift b/Sources/Realtime/RealtimeChannel+AsyncAwait.swift index 8a12a4d9d..39c6fcde9 100644 --- a/Sources/Realtime/RealtimeChannel+AsyncAwait.swift +++ b/Sources/Realtime/RealtimeChannel+AsyncAwait.swift @@ -183,12 +183,6 @@ extension RealtimeChannelV2 { return stream } - - /// Listen for broadcast messages sent by other clients within the same channel under a specific `event`. - @available(*, deprecated, renamed: "broadcastStream(event:)") - public func broadcast(event: String) -> AsyncStream { - broadcastStream(event: event) - } } // Helper to work around type ambiguity in macOS 13 diff --git a/Sources/Realtime/RealtimeChannelV2.swift b/Sources/Realtime/RealtimeChannelV2.swift index bf0b3b467..baf0de044 100644 --- a/Sources/Realtime/RealtimeChannelV2.swift +++ b/Sources/Realtime/RealtimeChannelV2.swift @@ -160,13 +160,6 @@ public final class RealtimeChannelV2: Sendable, RealtimeChannelProtocol { throw RealtimeError.maxRetryAttemptsReached } - /// Subscribes to the channel. - @available(*, deprecated, message: "Use `subscribeWithError` instead") - @MainActor - public func subscribe() async { - try? await subscribeWithError() - } - /// Calculates retry delay with exponential backoff and jitter private func calculateRetryDelay(for attempt: Int) -> TimeInterval { let baseDelay: TimeInterval = 1.0 diff --git a/Sources/Realtime/Types.swift b/Sources/Realtime/Types.swift index 30d625e06..7b418449d 100644 --- a/Sources/Realtime/Types.swift +++ b/Sources/Realtime/Types.swift @@ -110,3 +110,29 @@ extension HTTPField.Name { public enum LogLevel: String, Sendable { case info, warn, error } + +/// Represents the different events that can be sent through +/// a channel regarding a Channel's lifecycle. +enum ChannelEvent { + static let join = "phx_join" + static let leave = "phx_leave" + static let close = "phx_close" + static let error = "phx_error" + static let reply = "phx_reply" + static let system = "system" + static let broadcast = "broadcast" + static let accessToken = "access_token" + static let presence = "presence" + static let presenceDiff = "presence_diff" + static let presenceState = "presence_state" + static let postgresChanges = "postgres_changes" + + static let heartbeat = "heartbeat" + + static func isLifecyleEvent(_ event: String) -> Bool { + switch event { + case join, leave, reply, error, close: true + default: false + } + } +} diff --git a/Sources/Storage/Codable.swift b/Sources/Storage/Codable.swift index 37995c77c..092e98ee4 100644 --- a/Sources/Storage/Codable.swift +++ b/Sources/Storage/Codable.swift @@ -9,8 +9,7 @@ import ConcurrencyExtras import Foundation extension JSONEncoder { - @available(*, deprecated, message: "Access to storage encoder is going to be removed.") - public static let defaultStorageEncoder: JSONEncoder = { + static let defaultStorageEncoder: JSONEncoder = { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase return encoder @@ -20,8 +19,7 @@ extension JSONEncoder { } extension JSONDecoder { - @available(*, deprecated, message: "Access to storage decoder is going to be removed.") - public static let defaultStorageDecoder: JSONDecoder = { + static let defaultStorageDecoder: JSONDecoder = { JSONDecoder.supabase() }() } diff --git a/Sources/Storage/Deprecated.swift b/Sources/Storage/Deprecated.swift deleted file mode 100644 index ed39b06b4..000000000 --- a/Sources/Storage/Deprecated.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// Deprecated.swift -// -// -// Created by Guilherme Souza on 16/01/24. -// - -import Foundation - -extension StorageClientConfiguration { - @available( - *, - deprecated, - message: - "Replace usages of this initializer with new init(url:headers:encoder:decoder:session:logger)" - ) - public init( - url: URL, - headers: [String: String], - encoder: JSONEncoder = .defaultStorageEncoder, - decoder: JSONDecoder = .defaultStorageDecoder, - session: StorageHTTPSession = .init() - ) { - self.init( - url: url, - headers: headers, - encoder: encoder, - decoder: decoder, - session: session, - logger: nil - ) - } -} - -extension StorageFileApi { - @_disfavoredOverload - @available(*, deprecated, message: "Please use method that returns FileUploadResponse.") - @discardableResult - public func upload( - path: String, - file: Data, - options: FileOptions = FileOptions() - ) async throws -> String { - try await upload(path: path, file: file, options: options).fullPath - } - - @_disfavoredOverload - @available(*, deprecated, message: "Please use method that returns FileUploadResponse.") - @discardableResult - public func update( - path: String, - file: Data, - options: FileOptions = FileOptions() - ) async throws -> String { - try await update(path: path, file: file, options: options).fullPath - } - - @_disfavoredOverload - @available(*, deprecated, message: "Please use method that returns FileUploadResponse.") - @discardableResult - public func uploadToSignedURL( - path: String, - token: String, - file: Data, - options: FileOptions = FileOptions() - ) async throws -> String { - try await uploadToSignedURL(path: path, token: token, file: file, options: options).fullPath - } - - @available(*, deprecated, renamed: "upload(_:data:options:)") - @discardableResult - public func upload( - path: String, - file: Data, - options: FileOptions = FileOptions() - ) async throws -> FileUploadResponse { - try await upload(path, data: file, options: options) - } - - @available(*, deprecated, renamed: "update(_:data:options:)") - @discardableResult - public func update( - path: String, - file: Data, - options: FileOptions = FileOptions() - ) async throws -> FileUploadResponse { - try await update(path, data: file, options: options) - } - - @available(*, deprecated, renamed: "updateToSignedURL(_:token:data:options:)") - @discardableResult - public func uploadToSignedURL( - path: String, - token: String, - file: Data, - options: FileOptions = FileOptions() - ) async throws -> SignedURLUploadResponse { - try await uploadToSignedURL(path, token: token, data: file, options: options) - } -} - -@available( - *, - deprecated, - message: - "File was deprecated and it isn't used in the package anymore, if you're using it on your application, consider replacing it as it will be removed on the next major release." -) -public struct File: Hashable, Equatable { - public var name: String - public var data: Data - public var fileName: String? - public var contentType: String? - - public init(name: String, data: Data, fileName: String?, contentType: String?) { - self.name = name - self.data = data - self.fileName = fileName - self.contentType = contentType - } -} - -@available( - *, - deprecated, - renamed: "MultipartFormData", - message: - "FormData was deprecated in favor of MultipartFormData, and it isn't used in the package anymore, if you're using it on your application, consider replacing it as it will be removed on the next major release." -) -public class FormData { - var files: [File] = [] - var boundary: String - - public init(boundary: String = UUID().uuidString) { - self.boundary = boundary - } - - public func append(file: File) { - files.append(file) - } - - public var contentType: String { - "multipart/form-data; boundary=\(boundary)" - } - - public var data: Data { - var data = Data() - - for file in files { - data.append("--\(boundary)\r\n") - data.append("Content-Disposition: form-data; name=\"\(file.name)\"") - if let filename = file.fileName?.replacingOccurrences(of: "\"", with: "_") { - data.append("; filename=\"\(filename)\"") - } - data.append("\r\n") - if let contentType = file.contentType { - data.append("Content-Type: \(contentType)\r\n") - } - data.append("\r\n") - data.append(file.data) - data.append("\r\n") - } - - data.append("--\(boundary)--\r\n") - return data - } -} - -extension Data { - mutating func append(_ string: String) { - let data = string.data( - using: String.Encoding.utf8, - allowLossyConversion: true - ) - append(data!) - } -} diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index ba043c8b8..5db5a68ef 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -12,16 +12,14 @@ public struct StorageClientConfiguration: Sendable { public init( url: URL, headers: [String: String], - encoder: JSONEncoder = .defaultStorageEncoder, - decoder: JSONDecoder = .defaultStorageDecoder, session: StorageHTTPSession = .init(), logger: (any SupabaseLogger)? = nil, useNewHostname: Bool = false ) { self.url = url self.headers = headers - self.encoder = encoder - self.decoder = decoder + self.encoder = .defaultStorageEncoder + self.decoder = .defaultStorageDecoder self.session = session self.logger = logger self.useNewHostname = useNewHostname diff --git a/Sources/Supabase/Deprecated.swift b/Sources/Supabase/Deprecated.swift deleted file mode 100644 index 5043e4119..000000000 --- a/Sources/Supabase/Deprecated.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Deprecated.swift -// -// -// Created by Guilherme Souza on 15/05/24. -// - -import Foundation - -extension SupabaseClient { - /// Database client for Supabase. - @available( - *, - deprecated, - message: "Direct access to database is deprecated, please use one of the available methods such as, SupabaseClient.from(_:), SupabaseClient.rpc(_:params:), or SupabaseClient.schema(_:)." - ) - public var database: PostgrestClient { - rest - } - - /// Realtime client for Supabase - @available(*, deprecated, message: "Use realtimeV2") - public var realtime: RealtimeClient { - _realtime.value - } -} diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index b419a94e8..62bdb6122 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -68,8 +68,6 @@ public final class SupabaseClient: Sendable { } } - let _realtime: UncheckedSendable - /// Realtime client for Supabase public var realtimeV2: RealtimeClientV2 { mutableState.withValue { @@ -184,14 +182,6 @@ public final class SupabaseClient: Sendable { autoRefreshToken: options.auth.autoRefreshToken ) - _realtime = UncheckedSendable( - RealtimeClient( - supabaseURL.appendingPathComponent("/realtime/v1").absoluteString, - headers: _headers.dictionary, - params: _headers.dictionary - ) - ) - if options.auth.accessToken == nil { listenForAuthEvents() } @@ -244,7 +234,7 @@ public final class SupabaseClient: Sendable { /// Returns all Realtime channels. public var channels: [RealtimeChannelV2] { - Array(realtimeV2.subscriptions.values) + Array(realtimeV2.channels.values) } /// Creates a Realtime channel with Broadcast, Presence, and Postgres Changes. @@ -387,7 +377,6 @@ public final class SupabaseClient: Sendable { return nil } - realtime.setAuth(accessToken) await realtimeV2.setAuth(accessToken) } diff --git a/Supabase.xcworkspace/xcshareddata/xcschemes/Auth.xcscheme b/Supabase.xcworkspace/xcshareddata/xcschemes/Auth.xcscheme index f85d93183..a0cfddcd4 100644 --- a/Supabase.xcworkspace/xcshareddata/xcschemes/Auth.xcscheme +++ b/Supabase.xcworkspace/xcshareddata/xcschemes/Auth.xcscheme @@ -1,6 +1,6 @@ ( sut: AuthClient, action: () async throws -> T, diff --git a/Tests/AuthTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift index 92c5b5aac..89a850797 100644 --- a/Tests/AuthTests/RequestsTests.swift +++ b/Tests/AuthTests/RequestsTests.swift @@ -291,7 +291,6 @@ final class RequestsTests: XCTestCase { phone: "+1 202-918-2132", password: "another.pass", nonce: "abcdef", - emailChangeToken: "123456", data: ["custom_key": .string("custom_value")] ) ) @@ -424,17 +423,6 @@ final class RequestsTests: XCTestCase { } } - func testMFAEnrollLegacy() async throws { - let sut = makeSUT() - - Dependencies[sut.clientID].sessionStorage.store(.validSession) - - await assert { - _ = try await sut.mfa.enroll( - params: MFAEnrollParams(issuer: "supabase.com", friendlyName: "test")) - } - } - func testMFAEnrollTotp() async throws { let sut = makeSUT() diff --git a/Tests/IntegrationTests/PostgrestIntegrationTests.swift b/Tests/IntegrationTests/PostgrestIntegrationTests.swift index 6336fcfcf..5cddc4695 100644 --- a/Tests/IntegrationTests/PostgrestIntegrationTests.swift +++ b/Tests/IntegrationTests/PostgrestIntegrationTests.swift @@ -125,7 +125,7 @@ final class IntegrationTests: XCTestCase { try await client.from("users").insert(users).execute() let fetchedUsers: [User] = try await client.from("users").select() - .ilike("email", value: "johndoe+test%").execute().value + .ilike("email", pattern: "johndoe+test%").execute().value XCTAssertEqual( fetchedUsers[...], users[1 ... 2] From 4dce37b3595b457b9d228fcdf1f57efd652acdc6 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 7 Oct 2025 16:03:25 -0300 Subject: [PATCH 2/3] test: fix tests --- Tests/AuthTests/AuthClientTests.swift | 4 ++-- .../__Snapshots__/RequestsTests/testUpdateUser.1.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/AuthTests/AuthClientTests.swift b/Tests/AuthTests/AuthClientTests.swift index 9125dd390..f95a43c32 100644 --- a/Tests/AuthTests/AuthClientTests.swift +++ b/Tests/AuthTests/AuthClientTests.swift @@ -1246,12 +1246,12 @@ final class AuthClientTests: XCTestCase { curl \ --request PUT \ --header "Authorization: Bearer accesstoken" \ - --header "Content-Length: 258" \ + --header "Content-Length: 228" \ --header "Content-Type: application/json" \ --header "X-Client-Info: auth-swift/0.0.0" \ --header "X-Supabase-Api-Version: 2024-01-01" \ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ - --data "{\"code_challenge\":\"hgJeigklONUI1pKSS98MIAbtJGaNu0zJU1iSiFOn2lY\",\"code_challenge_method\":\"s256\",\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"email_change_token\":\"123456\",\"nonce\":\"abcdef\",\"password\":\"another.pass\",\"phone\":\"+1 202-918-2132\"}" \ + --data "{\"code_challenge\":\"hgJeigklONUI1pKSS98MIAbtJGaNu0zJU1iSiFOn2lY\",\"code_challenge_method\":\"s256\",\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"nonce\":\"abcdef\",\"password\":\"another.pass\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/user" """# } diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt index 45eaa5f0f..7efb07af0 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt @@ -5,5 +5,5 @@ curl \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ - --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"email_change_token\":\"123456\",\"nonce\":\"abcdef\",\"password\":\"another.pass\",\"phone\":\"+1 202-918-2132\"}" \ + --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"nonce\":\"abcdef\",\"password\":\"another.pass\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/user" \ No newline at end of file From 157254f80840c97236e5827a040b5a0402a3a2ff Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 7 Oct 2025 16:33:27 -0300 Subject: [PATCH 3/3] docs: fix example apps not building --- Examples/Examples/MFAFlow.swift | 2 +- Examples/Examples/TodoListView.swift | 8 ++++---- Examples/SlackClone/ChannelStore.swift | 7 ++++++- Examples/SlackClone/MessageStore.swift | 7 ++++++- Examples/SlackClone/UserStore.swift | 7 ++++++- Examples/UserManagement/ProfileView.swift | 4 ++-- 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Examples/Examples/MFAFlow.swift b/Examples/Examples/MFAFlow.swift index 937f015af..3ed1c84cc 100644 --- a/Examples/Examples/MFAFlow.swift +++ b/Examples/Examples/MFAFlow.swift @@ -92,7 +92,7 @@ struct MFAEnrollView: View { .task { do { error = nil - enrollResponse = try await supabase.auth.mfa.enroll(params: MFAEnrollParams()) + enrollResponse = try await supabase.auth.mfa.enroll(params: .totp()) } catch { self.error = error } diff --git a/Examples/Examples/TodoListView.swift b/Examples/Examples/TodoListView.swift index c0e73ac95..9cf89beee 100644 --- a/Examples/Examples/TodoListView.swift +++ b/Examples/Examples/TodoListView.swift @@ -81,7 +81,7 @@ struct TodoListView: View { do { error = nil todos = try await IdentifiedArrayOf( - uniqueElements: supabase.database.from("todos") + uniqueElements: supabase.from("todos") .select() .execute() .value as [Todo] @@ -105,7 +105,7 @@ struct TodoListView: View { isComplete: updatedTodo.isComplete, ownerID: auth.currentUserID ) - updatedTodo = try await supabase.database.from("todos") + updatedTodo = try await supabase.from("todos") .update(updateRequest, returning: .representation) .eq("id", value: updatedTodo.id) .single() @@ -129,9 +129,9 @@ struct TodoListView: View { todos.remove(atOffsets: offset) - try await supabase.database.from("todos") + try await supabase.from("todos") .delete() - .in("id", value: todosToDelete.map(\.id)) + .in("id", values: todosToDelete.map(\.id)) .execute() } catch { self.error = error diff --git a/Examples/SlackClone/ChannelStore.swift b/Examples/SlackClone/ChannelStore.swift index e954cf93e..849591997 100644 --- a/Examples/SlackClone/ChannelStore.swift +++ b/Examples/SlackClone/ChannelStore.swift @@ -27,7 +27,12 @@ final class ChannelStore { let insertions = channel.postgresChange(InsertAction.self, table: "channels") let deletions = channel.postgresChange(DeleteAction.self, table: "channels") - await channel.subscribe() + do { + try await channel.subscribeWithError() + } catch { + print("Error subscribing to channel: \(error)") + return + } Task { for await insertion in insertions { diff --git a/Examples/SlackClone/MessageStore.swift b/Examples/SlackClone/MessageStore.swift index 6282a20e6..89820c623 100644 --- a/Examples/SlackClone/MessageStore.swift +++ b/Examples/SlackClone/MessageStore.swift @@ -109,7 +109,12 @@ final class MessageStore { let updates = channel.postgresChange(UpdateAction.self, table: "messages") let deletions = channel.postgresChange(DeleteAction.self, table: "messages") - await channel.subscribe() + do { + try await channel.subscribeWithError() + } catch { + print("Error subscribing to channel: \(error)") + return + } Task { for await insertion in insertions { diff --git a/Examples/SlackClone/UserStore.swift b/Examples/SlackClone/UserStore.swift index f14064bc3..484890e0d 100644 --- a/Examples/SlackClone/UserStore.swift +++ b/Examples/SlackClone/UserStore.swift @@ -24,7 +24,12 @@ final class UserStore { let presences = channel.presenceChange() - await channel.subscribe() + do { + try await channel.subscribeWithError() + } catch { + print("Error subscribing to channel: \(error)") + return + } Task { let statusChange = channel.statusChange diff --git a/Examples/UserManagement/ProfileView.swift b/Examples/UserManagement/ProfileView.swift index caf70cafa..da84fdd8f 100644 --- a/Examples/UserManagement/ProfileView.swift +++ b/Examples/UserManagement/ProfileView.swift @@ -172,8 +172,8 @@ struct ProfileView: View { try await supabase.storage .from("avatars") .upload( - path: filePath, - file: data, + filePath, + data: data, options: FileOptions(contentType: "image/jpeg", upsert: true) )