From 79f8661c30cbe568baced88e4cff320dc5d37af2 Mon Sep 17 00:00:00 2001 From: Brian R Date: Wed, 2 Aug 2023 17:26:55 -0400 Subject: [PATCH 1/6] fix presence diff event set to wrong ChannelEvent --- Sources/Realtime/Presence.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Realtime/Presence.swift b/Sources/Realtime/Presence.swift index 412513e..96bfbf9 100644 --- a/Sources/Realtime/Presence.swift +++ b/Sources/Realtime/Presence.swift @@ -109,7 +109,7 @@ public final class Presence { /// phoenix events "presence_state" and "presence_diff" public static let defaults = Options(events: [ .state: .presenceState, - .diff: .presenceState, + .diff: .presenceDiff, ]) public init(events: [Events: ChannelEvent]) { From f082ab35884d39517c222b2481a05549fc19ad2d Mon Sep 17 00:00:00 2001 From: Brian R Date: Wed, 2 Aug 2023 17:30:44 -0400 Subject: [PATCH 2/6] add additional Broadcast and Presence functions with Encodable payload --- Sources/Realtime/Channel.swift | 41 ++++++++++++++++++++++++++++++++- Sources/Realtime/Defaults.swift | 7 ++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Sources/Realtime/Channel.swift b/Sources/Realtime/Channel.swift index 700885f..c55a113 100644 --- a/Sources/Realtime/Channel.swift +++ b/Sources/Realtime/Channel.swift @@ -694,6 +694,21 @@ extension Channel { return state == .leaving } } +// ---------------------------------------------------------------------- + +// MARK: - Encodable Payload + +// ---------------------------------------------------------------------- +fileprivate extension Encodable { + /// Encodes to a payload + /// - parameter encoder: The encoder to use to encode the payload + /// - returns: The encoded payload + func payload(encoder: JSONEncoder = Defaults.encoder) throws -> Payload { + let data = try encoder.encode(self) + return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! Payload + } +} + // ---------------------------------------------------------------------- @@ -701,6 +716,9 @@ extension Channel { // ---------------------------------------------------------------------- extension Channel { + /// Broadcasts the payload to all other members of the channel + /// - parameter event: The event to broadcast + /// - parameter payload: The payload to broadcast @discardableResult public func broadcast(event: String, payload: Payload) -> Push { self.push(.broadcast, payload: [ @@ -709,6 +727,16 @@ extension Channel { "payload": payload ]) } + + /// Broadcasts the encodable payload to all other members of the channel + /// - parameter event: The event to broadcast + /// - parameter payload: The payload to broadcast + /// - parameter encoder: The encoder to use to encode the payload + /// - throws: Throws an error if the payload cannot be encoded + @discardableResult + public func broadcast(event: String, payload: Encodable, encoder: JSONEncoder = Defaults.encoder) throws -> Push { + self.broadcast(event: event, payload: try payload.payload(encoder: encoder)) + } } // ---------------------------------------------------------------------- @@ -717,6 +745,8 @@ extension Channel { // ---------------------------------------------------------------------- extension Channel { + /// Share presence state, available to all channel members via sync + /// - parameter payload: The payload to broadcast @discardableResult public func track(payload: Payload) -> Push { self.push(.presence, payload: [ @@ -726,6 +756,16 @@ extension Channel { ]) } + /// Share presence state, available to all channel members via sync + /// - parameter payload: The payload to broadcast + /// - parameter encoder: The encoder to use to encode the payload + /// - throws: Throws an error if the payload cannot be encoded + @discardableResult + public func track(payload: Encodable, encoder: JSONEncoder = Defaults.encoder) throws -> Push { + self.track(payload: try payload.payload(encoder: encoder)) + } + + /// Remove presence state for given channel @discardableResult public func untrack() -> Push { self.push(.presence, payload: [ @@ -733,5 +773,4 @@ extension Channel { "event": "untrack" ]) } - } diff --git a/Sources/Realtime/Defaults.swift b/Sources/Realtime/Defaults.swift index c7cd7cc..4a5b7f3 100644 --- a/Sources/Realtime/Defaults.swift +++ b/Sources/Realtime/Defaults.swift @@ -42,6 +42,13 @@ public enum Defaults { } public static let vsn = "2.0.0" + + /// Default encoder, with supabase date encoding strategy + public static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + return encoder + }() /// Default encode function, utilizing JSONSerialization.data public static let encode: (Any) -> Data = { json in From e4fd4f84f511aa113f1c996de8b367ae3ef1c196 Mon Sep 17 00:00:00 2001 From: Brian R Date: Wed, 2 Aug 2023 19:43:24 -0400 Subject: [PATCH 3/6] add decode extensions to Payload and Presence.State & add BroadcastPayload type --- Sources/Realtime/Channel.swift | 70 ++++++++++++++++++++++++++++----- Sources/Realtime/Defaults.swift | 13 +++--- Sources/Realtime/Presence.swift | 20 ++++++++++ 3 files changed, 87 insertions(+), 16 deletions(-) diff --git a/Sources/Realtime/Channel.swift b/Sources/Realtime/Channel.swift index c55a113..968cc22 100644 --- a/Sources/Realtime/Channel.swift +++ b/Sources/Realtime/Channel.swift @@ -696,16 +696,38 @@ extension Channel { } // ---------------------------------------------------------------------- -// MARK: - Encodable Payload +// MARK: - Codable Payload // ---------------------------------------------------------------------- -fileprivate extension Encodable { - /// Encodes to a payload + +extension Payload { + + /// Initializes a payload from a given value + /// - parameter value: The value to encode /// - parameter encoder: The encoder to use to encode the payload - /// - returns: The encoded payload - func payload(encoder: JSONEncoder = Defaults.encoder) throws -> Payload { - let data = try encoder.encode(self) - return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! Payload + /// - throws: Throws an error if the payload cannot be encoded + init(_ value: T, encoder: JSONEncoder = Defaults.encoder) throws { + let data = try encoder.encode(value) + self = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! Payload + } + + /// Decodes the payload to a given type + /// - parameter type: The type to decode to + /// - parameter decoder: The decoder to use to decode the payload + /// - returns: The decoded payload + /// - throws: Throws an error if the payload cannot be decoded + public func decode(to type: T.Type, decoder: JSONDecoder = Defaults.decoder) throws -> T { + let data = try JSONSerialization.data(withJSONObject: self) + return try decoder.decode(type, from: data) + } + + /// Decodes the payload to a given type + /// - parameter decoder: The decoder to use to decode the payload + /// - returns: The decoded payload + /// - throws: Throws an error if the payload cannot be decoded + public func decode(decoder: JSONDecoder = Defaults.decoder) throws -> T { + let data = try JSONSerialization.data(withJSONObject: self) + return try decoder.decode(T.self, from: data) } } @@ -715,6 +737,14 @@ fileprivate extension Encodable { // MARK: - Broadcast API // ---------------------------------------------------------------------- + +/// Represents the payload of a broadcast message +public struct BroadcastPayload { + public let type: String + public let event: String + public let payload: Payload +} + extension Channel { /// Broadcasts the payload to all other members of the channel /// - parameter event: The event to broadcast @@ -735,8 +765,30 @@ extension Channel { /// - throws: Throws an error if the payload cannot be encoded @discardableResult public func broadcast(event: String, payload: Encodable, encoder: JSONEncoder = Defaults.encoder) throws -> Push { - self.broadcast(event: event, payload: try payload.payload(encoder: encoder)) + self.broadcast(event: event, payload: try Payload(payload)) } + + /// Subscribes to broadcast events. Does not handle retain cycles. + /// + /// Example: + /// + /// let ref = channel.onBroadcast { [weak self] (broadcast) in + /// print(broadcast.event, broadcast.payload) + /// } + /// channel.off(.broadcast, ref1) + /// + /// Subscription returns a ref counter, which can be used later to + /// unsubscribe the exact event listener + /// - parameter callback: Called with the broadcast payload + /// - returns: Ref counter of the subscription. See `func off()` + @discardableResult + public func onBroadcast(callback: @escaping (BroadcastPayload) -> Void) -> Int { + self.on(.broadcast, callback: { message in + let payload = BroadcastPayload(type: message.payload["type"] as! String, event: message.payload["event"] as! String, payload: message.payload["payload"] as! Payload) + callback(payload) + }) + } + } // ---------------------------------------------------------------------- @@ -762,7 +814,7 @@ extension Channel { /// - throws: Throws an error if the payload cannot be encoded @discardableResult public func track(payload: Encodable, encoder: JSONEncoder = Defaults.encoder) throws -> Push { - self.track(payload: try payload.payload(encoder: encoder)) + self.track(payload: try Payload(payload)) } /// Remove presence state for given channel diff --git a/Sources/Realtime/Defaults.swift b/Sources/Realtime/Defaults.swift index 4a5b7f3..0a7eefe 100644 --- a/Sources/Realtime/Defaults.swift +++ b/Sources/Realtime/Defaults.swift @@ -43,12 +43,8 @@ public enum Defaults { public static let vsn = "2.0.0" - /// Default encoder, with supabase date encoding strategy - public static let encoder: JSONEncoder = { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 - return encoder - }() + /// Default encoder + public static let encoder: JSONEncoder = JSONEncoder() /// Default encode function, utilizing JSONSerialization.data public static let encode: (Any) -> Data = { json in @@ -60,7 +56,10 @@ public enum Defaults { options: JSONSerialization.WritingOptions() ) } - + + /// Default decoder + public static let decoder: JSONDecoder = JSONDecoder() + /// Default decode function, utilizing JSONSerialization.jsonObject public static let decode: (Data) -> Any? = { data in guard diff --git a/Sources/Realtime/Presence.swift b/Sources/Realtime/Presence.swift index 96bfbf9..ac1417a 100644 --- a/Sources/Realtime/Presence.swift +++ b/Sources/Realtime/Presence.swift @@ -409,3 +409,23 @@ public final class Presence { return presences.map(transformer) } } + + + +extension Presence.State { + + public func decode(_ type: T.Type, decoder: JSONDecoder = Defaults.decoder) throws -> [String: [T]] { + var decoded: [String: [T]] = [:] + try self.forEach { key, map in + let metas: [Presence.Meta] = map["metas"]! + let data = try JSONSerialization.data(withJSONObject: metas) + decoded[key] = try decoder.decode([T].self, from: data) + } + return decoded + } + + public func decode(decoder: JSONDecoder = Defaults.decoder) throws -> [String: [T]] { + return try decode(T.self, decoder: decoder) + } + +} From 3acee011080a893a2039d94cc71e647f4eb9af03 Mon Sep 17 00:00:00 2001 From: Brian R Date: Wed, 2 Aug 2023 19:49:50 -0400 Subject: [PATCH 4/6] add missing message parameter --- Sources/Realtime/Channel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Realtime/Channel.swift b/Sources/Realtime/Channel.swift index 968cc22..997d8e6 100644 --- a/Sources/Realtime/Channel.swift +++ b/Sources/Realtime/Channel.swift @@ -772,7 +772,7 @@ extension Channel { /// /// Example: /// - /// let ref = channel.onBroadcast { [weak self] (broadcast) in + /// let ref = channel.onBroadcast { [weak self] (message,broadcast) in /// print(broadcast.event, broadcast.payload) /// } /// channel.off(.broadcast, ref1) @@ -782,10 +782,10 @@ extension Channel { /// - parameter callback: Called with the broadcast payload /// - returns: Ref counter of the subscription. See `func off()` @discardableResult - public func onBroadcast(callback: @escaping (BroadcastPayload) -> Void) -> Int { + public func onBroadcast(callback: @escaping (Message,BroadcastPayload) -> Void) -> Int { self.on(.broadcast, callback: { message in let payload = BroadcastPayload(type: message.payload["type"] as! String, event: message.payload["event"] as! String, payload: message.payload["payload"] as! Payload) - callback(payload) + callback(message, payload) }) } From 738def4b3de974a9267b1cd188c9d7305524bde7 Mon Sep 17 00:00:00 2001 From: Brian R Date: Fri, 4 Aug 2023 15:05:47 -0400 Subject: [PATCH 5/6] remove unneccessary decode functions & create PushStatus type and add in neccessary locations --- Sources/Realtime/Channel.swift | 22 +++++++--------------- Sources/Realtime/Defaults.swift | 25 +++++++++++++++++++++++++ Sources/Realtime/Message.swift | 7 +++++-- Sources/Realtime/Presence.swift | 33 +++++++++++++++++++++------------ Sources/Realtime/Push.swift | 18 +++++++++--------- 5 files changed, 67 insertions(+), 38 deletions(-) diff --git a/Sources/Realtime/Channel.swift b/Sources/Realtime/Channel.swift index 997d8e6..0953103 100644 --- a/Sources/Realtime/Channel.swift +++ b/Sources/Realtime/Channel.swift @@ -168,7 +168,7 @@ public class Channel { ) /// Handle when a response is received after join() - joinPush.delegateReceive("ok", to: self) { (self, _) in + joinPush.delegateReceive(.ok, to: self) { (self, _) in // Mark the Channel as joined self.state = ChannelState.joined @@ -181,13 +181,13 @@ public class Channel { } // Perform if Channel errors while attempting to joi - joinPush.delegateReceive("error", to: self) { (self, _) in + 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 + 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" @@ -553,12 +553,12 @@ public class Channel { // 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) + .receive(.ok, delegated: onCloseDelegate) + .receive(.timeout, delegated: onCloseDelegate) leavePush.send() // If the Channel cannot send push events, trigger a success locally - if !canPush { leavePush.trigger("ok", payload: [:]) } + if !canPush { leavePush.trigger(.ok, payload: [:]) } // Return the push so it can be bound to return leavePush @@ -716,19 +716,11 @@ extension Payload { /// - parameter decoder: The decoder to use to decode the payload /// - returns: The decoded payload /// - throws: Throws an error if the payload cannot be decoded - public func decode(to type: T.Type, decoder: JSONDecoder = Defaults.decoder) throws -> T { + public func decode(to type: T.Type = T.self, decoder: JSONDecoder = Defaults.decoder) throws -> T { let data = try JSONSerialization.data(withJSONObject: self) return try decoder.decode(type, from: data) } - /// Decodes the payload to a given type - /// - parameter decoder: The decoder to use to decode the payload - /// - returns: The decoded payload - /// - throws: Throws an error if the payload cannot be decoded - public func decode(decoder: JSONDecoder = Defaults.decoder) throws -> T { - let data = try JSONSerialization.data(withJSONObject: self) - return try decoder.decode(T.self, from: data) - } } diff --git a/Sources/Realtime/Defaults.swift b/Sources/Realtime/Defaults.swift index 0a7eefe..adf54d6 100644 --- a/Sources/Realtime/Defaults.swift +++ b/Sources/Realtime/Defaults.swift @@ -234,3 +234,28 @@ public struct ChannelOptions { self.broadcastAcknowledge = broadcastAcknowledge } } + +/// Represents the different status of a push +public enum PushStatus: RawRepresentable { + case ok + case error + case timeout + + public var rawValue: String { + switch self { + case .ok: return "ok" + case .error: return "error" + case .timeout: return "timeout" + } + } + + public init?(rawValue: String) { + switch rawValue { + case "ok": self = .ok + case "error": self = .error + case "timeout": self = .timeout + default: return nil + } + } + +} diff --git a/Sources/Realtime/Message.swift b/Sources/Realtime/Message.swift index f5d9dba..5047203 100644 --- a/Sources/Realtime/Message.swift +++ b/Sources/Realtime/Message.swift @@ -49,8 +49,11 @@ public class Message { /// ```swift /// message.payload["status"] /// ``` - public var status: String? { - return rawPayload["status"] as? String + public var status: PushStatus? { + guard let status = rawPayload["status"] as? String else { + return nil + } + return PushStatus(rawValue: status) } init( diff --git a/Sources/Realtime/Presence.swift b/Sources/Realtime/Presence.swift index ac1417a..3991244 100644 --- a/Sources/Realtime/Presence.swift +++ b/Sources/Realtime/Presence.swift @@ -411,21 +411,30 @@ public final class Presence { } +extension Presence.Map { + + /// Decodes the presence metadata to an array of the specified type. + /// - parameter type: The type to decode to. + /// - parameter decoder: The decoder to use. + /// - returns: The decoded values. + /// - throws: Any error that occurs during decoding. + public func decode(to type: T.Type = T.self, decoder: JSONDecoder = Defaults.decoder) throws -> [T] { + let metas: [Presence.Meta] = self["metas"]! + let data = try JSONSerialization.data(withJSONObject: metas) + return try decoder.decode([T].self, from: data) + } -extension Presence.State { +} - public func decode(_ type: T.Type, decoder: JSONDecoder = Defaults.decoder) throws -> [String: [T]] { - var decoded: [String: [T]] = [:] - try self.forEach { key, map in - let metas: [Presence.Meta] = map["metas"]! - let data = try JSONSerialization.data(withJSONObject: metas) - decoded[key] = try decoder.decode([T].self, from: data) - } - return decoded - } +extension Presence.State { - public func decode(decoder: JSONDecoder = Defaults.decoder) throws -> [String: [T]] { - return try decode(T.self, decoder: decoder) + /// Decodes the presence metadata to a dictionary of arrays of the specified type. + /// - parameter type: The type to decode to. + /// - parameter decoder: The decoder to use. + /// - returns: The dictionary of decoded values. + /// - throws: Any error that occurs during decoding. + public func decode(to type: T.Type = T.self, decoder: JSONDecoder = Defaults.decoder) throws -> [String: [T]] { + return try mapValues { try $0.decode(decoder: decoder) } } } diff --git a/Sources/Realtime/Push.swift b/Sources/Realtime/Push.swift index 7d62a38..70c288f 100644 --- a/Sources/Realtime/Push.swift +++ b/Sources/Realtime/Push.swift @@ -44,7 +44,7 @@ public class Push { var timeoutWorkItem: DispatchWorkItem? /// Hooks into a Push. Where .receive("ok", callback(Payload)) are stored - var receiveHooks: [String: [Delegated]] + var receiveHooks: [PushStatus: [Delegated]] /// True if the Push has been sent var sent: Bool @@ -89,7 +89,7 @@ public class Push { /// 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 } + guard !hasReceived(status: .timeout) else { return } startTimeout() sent = true @@ -120,7 +120,7 @@ public class Push { /// - parameter callback: Callback to fire when the status is recevied @discardableResult public func receive( - _ status: String, + _ status: PushStatus, callback: @escaping ((Message) -> Void) ) -> Push { var delegated = Delegated() @@ -146,7 +146,7 @@ public class Push { /// - parameter callback: Callback to fire when the status is recevied @discardableResult public func delegateReceive( - _ status: String, + _ status: PushStatus, to owner: Target, callback: @escaping ((Target, Message) -> Void) ) -> Push { @@ -158,7 +158,7 @@ public class Push { /// Shared behavior between `receive` calls @discardableResult - internal func receive(_ status: String, delegated: Delegated) -> Push { + internal 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 = receivedMessage { delegated.call(receivedMessage) @@ -188,7 +188,7 @@ public class Push { /// /// - parameter status: Status which was received, e.g. "ok", "error", "timeout" /// - parameter response: Response that was received - private func matchReceive(_ status: String, message: Message) { + private func matchReceive(_ status: PushStatus, message: Message) { receiveHooks[status]?.forEach { $0.call(message) } } @@ -237,7 +237,7 @@ public class Push { /// Setup and start the Timeout timer. let workItem = DispatchWorkItem { - self.trigger("timeout", payload: [:]) + self.trigger(.timeout, payload: [:]) } timeoutWorkItem = workItem @@ -248,12 +248,12 @@ public class Push { /// /// - parameter status: Status to check /// - return: True if given status has been received by the Push. - internal func hasReceived(status: String) -> Bool { + internal func hasReceived(status: PushStatus) -> Bool { return receivedMessage?.status == status } /// Triggers an event to be sent though the Channel - internal func trigger(_ status: String, payload: Payload) { + internal func trigger(_ status: PushStatus, payload: Payload) { /// If there is no ref event, then there is nothing to trigger on the channel guard let refEvent = refEvent else { return } From 23e4d875fa80cd8edec91a5f324a5ac9e53b86a1 Mon Sep 17 00:00:00 2001 From: Brian R <44987647+foodisbeast@users.noreply.github.com> Date: Sun, 6 Aug 2023 23:36:02 -0400 Subject: [PATCH 6/6] Simply PushStatus enum Co-authored-by: Guilherme Souza --- Sources/Realtime/Defaults.swift | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/Sources/Realtime/Defaults.swift b/Sources/Realtime/Defaults.swift index adf54d6..897acd9 100644 --- a/Sources/Realtime/Defaults.swift +++ b/Sources/Realtime/Defaults.swift @@ -236,26 +236,8 @@ public struct ChannelOptions { } /// Represents the different status of a push -public enum PushStatus: RawRepresentable { +public enum PushStatus: String { case ok case error case timeout - - public var rawValue: String { - switch self { - case .ok: return "ok" - case .error: return "error" - case .timeout: return "timeout" - } - } - - public init?(rawValue: String) { - switch rawValue { - case "ok": self = .ok - case "error": self = .error - case "timeout": self = .timeout - default: return nil - } - } - }