From 91eea96308bab7ced8a615b18e1bce840116efb9 Mon Sep 17 00:00:00 2001 From: Erik Little Date: Sat, 30 Sep 2017 11:33:39 -0400 Subject: [PATCH] Make a bunch of things public. Fixes #803 --- Source/SocketIO/Ack/SocketAckEmitter.swift | 8 +- .../SocketIO/Client/SocketEventHandler.swift | 27 ++- Source/SocketIO/Client/SocketIOClient.swift | 98 ++++++---- .../SocketIO/Client/SocketIOClientSpec.swift | 167 +++++++++++++++++- Source/SocketIO/Parse/SocketPacket.swift | 46 +++-- Source/SocketIO/Parse/SocketParsable.swift | 39 +++- 6 files changed, 313 insertions(+), 72 deletions(-) diff --git a/Source/SocketIO/Ack/SocketAckEmitter.swift b/Source/SocketIO/Ack/SocketAckEmitter.swift index b13e15a..d8a30c4 100644 --- a/Source/SocketIO/Ack/SocketAckEmitter.swift +++ b/Source/SocketIO/Ack/SocketAckEmitter.swift @@ -39,7 +39,13 @@ public final class SocketAckEmitter : NSObject { return ackNum != -1 } - init(socket: SocketIOClient, ackNum: Int) { + // MARK: Initializers + + /// Creates a new `SocketAckEmitter`. + /// + /// - parameter socket: The socket for this emitter. + /// - parameter ackNum: The ack number for this emitter. + public init(socket: SocketIOClient, ackNum: Int) { self.socket = socket self.ackNum = ackNum } diff --git a/Source/SocketIO/Client/SocketEventHandler.swift b/Source/SocketIO/Client/SocketEventHandler.swift index 5497f7f..4f75a5a 100644 --- a/Source/SocketIO/Client/SocketEventHandler.swift +++ b/Source/SocketIO/Client/SocketEventHandler.swift @@ -24,12 +24,27 @@ import Foundation -struct SocketEventHandler { - let event: String - let id: UUID - let callback: NormalCallback - - func executeCallback(with items: [Any], withAck ack: Int, withSocket socket: SocketIOClient) { +/// A wrapper around a handler. +public struct SocketEventHandler { + // MARK: Properties + + /// The event for this handler. + public let event: String + + /// A unique identifier for this handler. + public let id: UUID + + /// The actual handler function. + public let callback: NormalCallback + + // MARK: Methods + + /// Causes this handler to be executed. + /// + /// - parameter with: The data that this handler should be called with. + /// - parameter withAck: The ack number that this event expects. Pass -1 to say this event doesn't expect an ack. + /// - parameter withSocket: The socket that is calling this event. + public func executeCallback(with items: [Any], withAck ack: Int, withSocket socket: SocketIOClient) { callback(items, SocketAckEmitter(socket: socket, ackNum: ack)) } } diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index 540fed0..22b122d 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -35,26 +35,6 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So private static let logType = "SocketIOClient" - /// The engine for this client. - @objc - public private(set) var engine: SocketEngineSpec? - - /// The status of this client. - @objc - public private(set) var status = SocketIOClientStatus.notConnected { - didSet { - switch status { - case .connected: - reconnecting = false - currentReconnectAttempt = 0 - default: - break - } - - handleClientEvent(.statusChange, data: [status]) - } - } - /// If `true` then every time `connect` is called, a new engine will be created. @objc public var forceNew = false @@ -94,20 +74,46 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So @objc public var socketURL: URL - var ackHandlers = SocketAckManager() - /// A list of packets that are waiting for binary data. /// /// The way that socket.io works all data should be sent directly after each packet. /// So this should ideally be an array of one packet waiting for data. - var waitingPackets = [SocketPacket]() + /// + /// **This should not be modified directly.** + public var waitingPackets = [SocketPacket]() + + /// A handler that will be called on any event. + public private(set) var anyHandler: ((SocketAnyEvent) -> ())? + + /// The engine for this client. + @objc + public private(set) var engine: SocketEngineSpec? + + /// The array of handlers for this socket. + public private(set) var handlers = [SocketEventHandler]() + + /// The status of this client. + @objc + public private(set) var status = SocketIOClientStatus.notConnected { + didSet { + switch status { + case .connected: + reconnecting = false + currentReconnectAttempt = 0 + default: + break + } + + handleClientEvent(.statusChange, data: [status]) + } + } + + var ackHandlers = SocketAckManager() private(set) var currentAck = -1 private(set) var reconnectAttempts = -1 - private var anyHandler: ((SocketAnyEvent) -> ())? private var currentReconnectAttempt = 0 - private var handlers = [SocketEventHandler]() private var reconnecting = false // MARK: Initializers @@ -232,7 +238,9 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So /// Called when the client connects to a namespace. If the client was created with a namespace upfront, /// then this is only called when the client connects to that namespace. - func didConnect(toNamespace namespace: String) { + /// + /// - parameter toNamespace: The namespace that was connected to. + open func didConnect(toNamespace namespace: String) { DefaultSocketLogger.Logger.log("Socket connected", type: SocketIOClient.logType) status = .connected @@ -241,7 +249,9 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So } /// Called when the client has disconnected from socket.io. - func didDisconnect(reason: String) { + /// + /// - parameter reason: The reason for the disconnection. + open func didDisconnect(reason: String) { guard status != .disconnected else { return } DefaultSocketLogger.Logger.log("Disconnected: \(reason)", type: SocketIOClient.logType) @@ -361,8 +371,13 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So engine?.send(str, withData: packet.binary) } - // If the server wants to know that the client received data - func emitAck(_ ack: Int, with items: [Any]) { + /// Call when you wish to tell the server that you've received the event for `ack`. + /// + /// **You shouldn't need to call this directly.** Instead use an `SocketAckEmitter` that comes in an event callback. + /// + /// - parameter ack: The ack number. + /// - parameter with: The data for this ack. + open func emitAck(_ ack: Int, with items: [Any]) { guard status == .connected else { return } let packet = SocketPacket.packetFromEmit(items, id: ack, nsp: nsp, ack: true) @@ -419,8 +434,12 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So DefaultSocketLogger.Logger.log(reason, type: SocketIOClient.logType) } - // Called when the socket gets an ack for something it sent - func handleAck(_ ack: Int, data: [Any]) { + /// Called when socket.io has acked one of our emits. Causes the corresponding ack callback to be called. + /// + /// - parameter ack: The number for this ack. + /// - parameter data: The data sent back with this ack. + @objc + open func handleAck(_ ack: Int, data: [Any]) { guard status == .connected else { return } DefaultSocketLogger.Logger.log("Handling ack: \(ack) with data: \(data)", type: SocketIOClient.logType) @@ -428,12 +447,12 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So ackHandlers.executeAck(ack, with: data, onQueue: handleQueue) } - /// Causes an event to be handled, and any event handlers for that event to be called. + /// Called when we get an event from socket.io. /// - /// - parameter event: The event that is to be handled. - /// - parameter data: the data associated with this event. - /// - parameter isInternalMessage: If `true` event handlers for this event will be called regardless of status. - /// - parameter withAck: The ack number for this event. May be left out. + /// - parameter event: The name of the event. + /// - parameter data: The data that was sent with this event. + /// - parameter isInternalMessage: Whether this event was sent internally. If `true` it is always sent to handlers. + /// - parameter withAck: If > 0 then this event expects to get an ack back from the client. @objc open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { guard status == .connected || isInternalMessage else { return } @@ -447,7 +466,11 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So } } - func handleClientEvent(_ event: SocketClientEvent, data: [Any]) { + /// Called on socket.io specific events. + /// + /// - parameter event: The `SocketClientEvent`. + /// - parameter data: The data for this event. + open func handleClientEvent(_ event: SocketClientEvent, data: [Any]) { handleEvent(event.rawValue, data: data, isInternalMessage: true) } @@ -615,6 +638,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So } /// Removes all handlers. + /// /// Can be used after disconnecting to break any potential remaining retain cycles. @objc open func removeAllHandlers() { diff --git a/Source/SocketIO/Client/SocketIOClientSpec.swift b/Source/SocketIO/Client/SocketIOClientSpec.swift index a514637..5b301fb 100644 --- a/Source/SocketIO/Client/SocketIOClientSpec.swift +++ b/Source/SocketIO/Client/SocketIOClientSpec.swift @@ -23,41 +23,118 @@ // THE SOFTWARE. import Dispatch - +import Foundation /// Defines the interface for a SocketIOClient. -protocol SocketIOClientSpec : class { +public protocol SocketIOClientSpec : class { + // MARK: Properties + + /// A handler that will be called on any event. + var anyHandler: ((SocketAnyEvent) -> ())? { get } + /// The queue that all interaction with the client must be on. var handleQueue: DispatchQueue { get set } + /// The array of handlers for this socket. + var handlers: [SocketEventHandler] { get } + /// The namespace that this socket is currently connected to. /// /// **Must** start with a `/`. var nsp: String { get set } - /// A list of packets that are waiting for binary data. + /// The status of this client. + var status: SocketIOClientStatus { get } + + // MARK: Methods + + /// Connect to the server. The same as calling `connect(timeoutAfter:withHandler:)` with a timeout of 0. /// - /// The way that socket.io works all data should be sent directly after each packet. - /// So this should ideally be an array of one packet waiting for data. - var waitingPackets: [SocketPacket] { get set } + /// Only call after adding your event listeners, unless you know what you're doing. + func connect() + + /// Connect to the server. If we aren't connected after `timeoutAfter` seconds, then `withHandler` is called. + /// + /// Only call after adding your event listeners, unless you know what you're doing. + /// + /// - parameter timeoutAfter: The number of seconds after which if we are not connected we assume the connection + /// has failed. Pass 0 to never timeout. + /// - parameter withHandler: The handler to call when the client fails to connect. + func connect(timeoutAfter: Double, withHandler handler: (() -> ())?) /// Called when the client connects to a namespace. If the client was created with a namespace upfront, /// then this is only called when the client connects to that namespace. + /// + /// - parameter toNamespace: The namespace that was connected to. func didConnect(toNamespace namespace: String) /// Called when the client has disconnected from socket.io. + /// + /// - parameter reason: The reason for the disconnection. func didDisconnect(reason: String) /// Called when the client encounters an error. + /// + /// - parameter reason: The reason for the disconnection. func didError(reason: String) + /// Disconnects the socket. + func disconnect() + + /// Send an event to the server, with optional data items. + /// + /// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error` + /// will be emitted. The structure of the error data is `[eventName, items, theError]` + /// + /// - parameter event: The event to send. + /// - parameter items: The items to send with this event. May be left out. + func emit(_ event: String, _ items: SocketData...) + + /// Call when you wish to tell the server that you've received the event for `ack`. + /// + /// - parameter ack: The ack number. + /// - parameter with: The data for this ack. + func emitAck(_ ack: Int, with items: [Any]) + + /// Sends a message to the server, requesting an ack. + /// + /// **NOTE**: It is up to the server send an ack back, just calling this method does not mean the server will ack. + /// Check that your server's api will ack the event being sent. + /// + /// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error` + /// will be emitted. The structure of the error data is `[eventName, items, theError]` + /// + /// Example: + /// + /// ```swift + /// socket.emitWithAck("myEvent", 1).timingOut(after: 1) {data in + /// ... + /// } + /// ``` + /// + /// - parameter event: The event to send. + /// - parameter items: The items to send with this event. May be left out. + /// - returns: An `OnAckCallback`. You must call the `timingOut(after:)` method before the event will be sent. + func emitWithAck(_ event: String, _ items: SocketData...) -> OnAckCallback + /// Called when socket.io has acked one of our emits. Causes the corresponding ack callback to be called. + /// + /// - parameter ack: The number for this ack. + /// - parameter data: The data sent back with this ack. func handleAck(_ ack: Int, data: [Any]) /// Called when we get an event from socket.io. + /// + /// - parameter event: The name of the event. + /// - parameter data: The data that was sent with this event. + /// - parameter isInternalMessage: Whether this event was sent internally. If `true` it is always sent to handlers. + /// - parameter withAck: If > 0 then this event expects to get an ack back from the client. func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int) - /// Called on socket.io events. + /// Called on socket.io specific events. + /// + /// - parameter event: The `SocketClientEvent`. + /// - parameter data: The data for this event. func handleClientEvent(_ event: SocketClientEvent, data: [Any]) /// Call when you wish to leave a namespace and return to the default namespace. @@ -69,11 +146,81 @@ protocol SocketIOClientSpec : class { /// /// - parameter namespace: The namespace to join. func joinNamespace(_ namespace: String) + + /// Removes handler(s) for a client event. + /// + /// If you wish to remove a client event handler, call the `off(id:)` with the UUID received from its `on` call. + /// + /// - parameter clientEvent: The event to remove handlers for. + func off(clientEvent event: SocketClientEvent) + + /// Removes handler(s) based on an event name. + /// + /// If you wish to remove a specific event, call the `off(id:)` with the UUID received from its `on` call. + /// + /// - parameter event: The event to remove handlers for. + func off(_ event: String) + + /// Removes a handler with the specified UUID gotten from an `on` or `once` + /// + /// If you want to remove all events for an event, call the off `off(_:)` method with the event name. + /// + /// - parameter id: The UUID of the handler you wish to remove. + func off(id: UUID) + + /// Adds a handler for an event. + /// + /// - parameter event: The event name for this handler. + /// - parameter callback: The callback that will execute when this event is received. + /// - returns: A unique id for the handler that can be used to remove it. + func on(_ event: String, callback: @escaping NormalCallback) -> UUID + + /// Adds a handler for a client event. + /// + /// Example: + /// + /// ```swift + /// socket.on(clientEvent: .connect) {data, ack in + /// ... + /// } + /// ``` + /// + /// - parameter event: The event for this handler. + /// - parameter callback: The callback that will execute when this event is received. + /// - returns: A unique id for the handler that can be used to remove it. + func on(clientEvent event: SocketClientEvent, callback: @escaping NormalCallback) -> UUID + + /// Adds a single-use handler for a client event. + /// + /// - parameter clientEvent: The event for this handler. + /// - parameter callback: The callback that will execute when this event is received. + /// - returns: A unique id for the handler that can be used to remove it. + func once(clientEvent event: SocketClientEvent, callback: @escaping NormalCallback) -> UUID + + /// Adds a single-use handler for an event. + /// + /// - parameter event: The event name for this handler. + /// - parameter callback: The callback that will execute when this event is received. + /// - returns: A unique id for the handler that can be used to remove it. + func once(_ event: String, callback: @escaping NormalCallback) -> UUID + + /// Adds a handler that will be called on every event. + /// + /// - parameter handler: The callback that will execute whenever an event is received. + func onAny(_ handler: @escaping (SocketAnyEvent) -> ()) + + /// Tries to reconnect to the server. + func reconnect() + + /// Removes all handlers. + /// + /// Can be used after disconnecting to break any potential remaining retain cycles. + func removeAllHandlers() } -extension SocketIOClientSpec { +public extension SocketIOClientSpec { /// Default implementation. - func didError(reason: String) { + public func didError(reason: String) { DefaultSocketLogger.Logger.error("\(reason)", type: "SocketIOClient") handleClientEvent(.error, data: [reason]) @@ -82,6 +229,8 @@ extension SocketIOClientSpec { /// The set of events that are generated by the client. public enum SocketClientEvent : String { + // MARK: Cases + /// Emitted when the client connects. This is also called on a successful reconnection. A connect event gets one /// data item: the namespace that was connected to. /// diff --git a/Source/SocketIO/Parse/SocketPacket.swift b/Source/SocketIO/Parse/SocketPacket.swift index a8a8bf1..8aebaf8 100644 --- a/Source/SocketIO/Parse/SocketPacket.swift +++ b/Source/SocketIO/Parse/SocketPacket.swift @@ -25,22 +25,39 @@ import Foundation -struct SocketPacket { - enum PacketType: Int { +/// A struct that represents a socket.io packet. +public struct SocketPacket : CustomStringConvertible { + // MARK: PacketType enum + + /// The type of packets. + public enum PacketType: Int { case connect, disconnect, event, ack, error, binaryEvent, binaryAck } - private let placeholders: Int + // MARK: Properties private static let logType = "SocketPacket" - let nsp: String - let id: Int - let type: PacketType + /// The namespace for this packet. + public let nsp: String - var binary: [Data] - var data: [Any] - var args: [Any] { + /// If > 0 then this packet is using acking. + public let id: Int + + /// The type of this packet. + public let type: PacketType + + /// An array of binary data for this packet. + public internal(set) var binary: [Data] + + /// The data for this event. + /// + /// Note: This includes all data inside of the socket.io packet payload array, which includes the event name for + /// event type packets. + public internal(set) var data: [Any] + + /// Returns the payload for this packet, minus the event name if this is an event or binaryEvent type packet. + public var args: [Any] { if type == .event || type == .binaryEvent && data.count != 0 { return Array(data.dropFirst()) } else { @@ -48,16 +65,21 @@ struct SocketPacket { } } - var description: String { + private let placeholders: Int + + /// A string representation of this packet. + public var description: String { return "SocketPacket {type: \(String(type.rawValue)); data: " + "\(String(describing: data)); id: \(id); placeholders: \(placeholders); nsp: \(nsp)}" } - var event: String { + /// The event name for this packet. + public var event: String { return String(describing: data[0]) } - var packetString: String { + /// A string representation of this packet. + public var packetString: String { return createPacketString() } diff --git a/Source/SocketIO/Parse/SocketParsable.swift b/Source/SocketIO/Parse/SocketParsable.swift index 137491a..578c704 100644 --- a/Source/SocketIO/Parse/SocketParsable.swift +++ b/Source/SocketIO/Parse/SocketParsable.swift @@ -23,7 +23,19 @@ import Foundation /// Defines that a type will be able to parse socket.io-protocol messages. -protocol SocketParsable { +public protocol SocketParsable : class { + // MARK: Properties + + /// A list of packets that are waiting for binary data. + /// + /// The way that socket.io works all data should be sent directly after each packet. + /// So this should ideally be an array of one packet waiting for data. + /// + /// **This should not be modified directly.** + var waitingPackets: [SocketPacket] { get set } + + // MARK: Methods + /// Called when the engine has received some binary data that should be attached to a packet. /// /// Packets binary data should be sent directly after the packet that expects it, so there's confusion over @@ -39,18 +51,28 @@ protocol SocketParsable { func parseSocketMessage(_ message: String) } -enum SocketParsableError : Error { +/// Errors that can be thrown during parsing. +public enum SocketParsableError : Error { + // MARK: Cases + + /// Thrown when a packet received has an invalid data array, or is missing the data array. case invalidDataArray + + /// Thrown when an malformed packet is received. case invalidPacket + + /// Thrown when the parser receives an unknown packet type. case invalidPacketType } -extension SocketParsable where Self: SocketIOClientSpec { +public extension SocketParsable where Self: SocketIOClientSpec { private func isCorrectNamespace(_ nsp: String) -> Bool { return nsp == self.nsp } private func handleConnect(_ packetNamespace: String) { + // If we connected with a namespace, check if we've joined the default namespace first, then switch to the + // other namespace if packetNamespace == "/" && nsp != "/" { joinNamespace(nsp) } else { @@ -79,8 +101,11 @@ extension SocketParsable where Self: SocketIOClientSpec { } } - /// Parses a messsage from the engine, returning a complete SocketPacket or throwing. - func parseString(_ message: String) throws -> SocketPacket { + /// Parses a message from the engine, returning a complete SocketPacket or throwing. + /// + /// - parameter message: The message to parse. + /// - returns: A completed packet, or throwing. + internal func parseString(_ message: String) throws -> SocketPacket { var reader = SocketStringReader(message: message) guard let type = Int(reader.read(count: 1)).flatMap({ SocketPacket.PacketType(rawValue: $0) }) else { @@ -148,7 +173,7 @@ extension SocketParsable where Self: SocketIOClientSpec { /// Called when the engine has received a string that should be parsed into a socket.io packet. /// /// - parameter message: The string that needs parsing. - func parseSocketMessage(_ message: String) { + public func parseSocketMessage(_ message: String) { guard !message.isEmpty else { return } DefaultSocketLogger.Logger.log("Parsing \(message)", type: "SocketParser") @@ -171,7 +196,7 @@ extension SocketParsable where Self: SocketIOClientSpec { /// into the correct placeholder. /// /// - parameter data: The data that should be attached to a packet. - func parseBinaryData(_ data: Data) { + public func parseBinaryData(_ data: Data) { guard !waitingPackets.isEmpty else { DefaultSocketLogger.Logger.error("Got data when not remaking packet", type: "SocketParser") return