diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index d97bae1e..a0ff9c55 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -77,9 +77,28 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { /// The id of this socket.io connect. This is different from the sid of the engine.io connection. public private(set) var sid: String? + /// The id of this socket.io connect for connection state recovery. + public private(set) var pid: String? + + /// Offset of last socket.io event for connection state recovery. + public private(set) var lastEventOffset: String? + /// Boolean setted after connection to know if socket state is recovered or not. + public private(set) var recovered: Bool = false + + /// Array of events (or binary events) to handle when socket is connected and recover packets from server + public private(set) var savedEvents = [SocketPacket]() let ackHandlers = SocketAckManager() - var connectPayload: [String: Any]? + + private var _connectPayload: [String: Any]? + var connectPayload: [String: Any]? { + get { + getConnectionStateRecoveryPayload(with: _connectPayload) + } + set { + _connectPayload = newValue + } + } private(set) var currentAck = -1 @@ -131,7 +150,6 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { } status = .connecting - joinNamespace(withPayload: payload) switch manager.version { @@ -159,6 +177,16 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { } } + func getConnectionStateRecoveryPayload(with payload: [String: Any]?) -> [String: Any]? { + guard let pid else { return payload } + var recoveryPayload = payload ?? [:] + recoveryPayload["pid"] = pid + if let lastEventOffset { + recoveryPayload["offset"] = lastEventOffset + } + return recoveryPayload + } + func createOnAck(_ items: [Any], binary: Bool = true) -> OnAckCallback { currentAck += 1 @@ -171,12 +199,16 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { /// - parameter toNamespace: The namespace that was connected to. open func didConnect(toNamespace namespace: String, payload: [String: Any]?) { guard status != .connected else { return } + let pid = payload?["pid"] as? String + recovered = self.pid != nil && self.pid == pid + self.pid = pid + sid = payload?["sid"] as? String DefaultSocketLogger.Logger.log("Socket connected", type: logType) status = .connected - sid = payload?["sid"] as? String + handleSavedEventPackets() handleClientEvent(.connect, data: payload == nil ? [namespace] : [namespace, payload!]) } @@ -366,6 +398,11 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { for handler in handlers where handler.event == event { handler.executeCallback(with: data, withAck: ack, withSocket: self) } + + if !isInternalMessage && ack < 0 && pid != nil, + let eventOffset = data.last as? String { + self.lastEventOffset = eventOffset + } } /// Causes a client to handle a socket.io packet. The namespace for the packet must match the namespace of the @@ -377,6 +414,7 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { switch packet.type { case .event, .binaryEvent: + saveEventPacketIfNeeded(packet: packet, isInternalMessage: false) handleEvent(packet.event, data: packet.args, isInternalMessage: false, withAck: packet.id) case .ack, .binaryAck: handleAck(packet.id, data: packet.data) @@ -389,6 +427,29 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { } } + /// Called when we get an event from socket.io + /// Save it to event array if an event is sent before socket is set to connected status. + /// Do not save event if pid is nil (cannot recover events from server) + /// + /// - parameter packet: The packet to handle. + /// - parameter isInternalMessage: Whether this event was sent internally. If `true` ignore it. + open func saveEventPacketIfNeeded(packet: SocketPacket, isInternalMessage: Bool) { + guard status != .connected && !isInternalMessage && pid != nil else { return } + savedEvents.append(packet) + } + + /// Called when socket pass to connected state, handle events if socket recover data from server + open func handleSavedEventPackets() { + if recovered { + savedEvents.removeAll { packet in + handleEvent(packet.event, data: packet.args, isInternalMessage: false) + return true + } + } else { + savedEvents.removeAll() + } + } + /// Call when you wish to leave a namespace and disconnect this socket. open func leaveNamespace() { manager?.disconnectSocket(self) diff --git a/Source/SocketIO/Client/SocketIOClientSpec.swift b/Source/SocketIO/Client/SocketIOClientSpec.swift index 04b62faa..624bf5a6 100644 --- a/Source/SocketIO/Client/SocketIOClientSpec.swift +++ b/Source/SocketIO/Client/SocketIOClientSpec.swift @@ -56,6 +56,16 @@ public protocol SocketIOClientSpec : AnyObject { /// The id of this socket.io connect. This is different from the sid of the engine.io connection. var sid: String? { get } + /// The id of this socket.io connect for connection state recovery. + var pid: String? { get } + + /// Offset of last socket.io event for connection state recovery. + var lastEventOffset: String? { get } + /// Boolean setted after connection to know if socket state is recovered or not. + var recovered: Bool { get } + + /// Array of events (or binary events) to handle when socket is connected and recover packets from server + var savedEvents: [SocketPacket] { get } /// The status of this client. var status: SocketIOStatus { get } @@ -192,6 +202,17 @@ public protocol SocketIOClientSpec : AnyObject { /// - parameter packet: The packet to handle. func handlePacket(_ packet: SocketPacket) + /// Called when we get an event from socket.io + /// Save it to event array if an event is sent before socket is set to connected status. + /// Do not save event if pid is nil (cannot recover events from server) + /// + /// - parameter packet: The packet to handle. + /// - parameter isInternalMessage: Whether this event was sent internally. If `true` ignore it. + func saveEventPacketIfNeeded(packet: SocketPacket, isInternalMessage: Bool) + + /// Called when socket pass to connected state, handle events if socket recover data from server + func handleSavedEventPackets() + /// Call when you wish to leave a namespace and disconnect this socket. func leaveNamespace() diff --git a/Source/SocketIO/Engine/SocketEngine.swift b/Source/SocketIO/Engine/SocketEngine.swift index 1d1c071d..3445fea9 100644 --- a/Source/SocketIO/Engine/SocketEngine.swift +++ b/Source/SocketIO/Engine/SocketEngine.swift @@ -335,6 +335,15 @@ open class SocketEngine: NSObject, WebSocketDelegate, URLSessionDelegate, } } + /// Force close engine. + /// + /// - parameter reason: The reason for the disconnection. This is communicated up to the client. + open func close(reason: String) { + guard connected && !closed else { return closeOutEngine(reason: reason) } + DefaultSocketLogger.Logger.log("Engine is being force closed.", type: SocketEngine.logType) + closeOutEngine(reason: reason) + } + private func _disconnect(reason: String) { guard connected && !closed else { return closeOutEngine(reason: reason) } @@ -563,6 +572,7 @@ open class SocketEngine: NSObject, WebSocketDelegate, URLSessionDelegate, polling = true probing = false invalidated = false + session?.invalidateAndCancel() session = Foundation.URLSession(configuration: .default, delegate: sessionDelegate, delegateQueue: queue) sid = "" waitingForPoll = false @@ -714,6 +724,13 @@ open class SocketEngine: NSObject, WebSocketDelegate, URLSessionDelegate, if let error = error as? WSError { didError(reason: "\(error.message). code=\(error.code), type=\(error.type)") + } else if let error = error as? HTTPUpgradeError { + switch error { + case let .notAnUpgrade(int, _): + didError(reason: "notAnUpgrade. code=\(int)") + case .invalidData: + didError(reason: "invalidData") + } } else if let reason = error?.localizedDescription { didError(reason: reason) } else { diff --git a/Source/SocketIO/Engine/SocketEngineSpec.swift b/Source/SocketIO/Engine/SocketEngineSpec.swift index fc0aa58b..945031e7 100644 --- a/Source/SocketIO/Engine/SocketEngineSpec.swift +++ b/Source/SocketIO/Engine/SocketEngineSpec.swift @@ -113,6 +113,11 @@ public protocol SocketEngineSpec: AnyObject { /// - parameter reason: The reason for the disconnection. This is communicated up to the client. func disconnect(reason: String) + /// Force close engine. + /// + /// - parameter reason: The reason for the disconnection. This is communicated up to the client. + func close(reason: String) + /// Called to switch from HTTP long-polling to WebSockets. After calling this method the engine will be in /// WebSocket mode. /// diff --git a/Source/SocketIO/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift index d69aa11f..e1ba8f67 100644 --- a/Source/SocketIO/Manager/SocketManager.swift +++ b/Source/SocketIO/Manager/SocketManager.swift @@ -107,7 +107,7 @@ open class SocketManager: NSObject, SocketManagerSpec, SocketParsable, SocketDat public var randomizationFactor = 0.5 /// The status of this manager. - public private(set) var status: SocketIOStatus = .notConnected { + public var status: SocketIOStatus = .notConnected { didSet { switch status { case .connected: @@ -242,6 +242,16 @@ open class SocketManager: NSObject, SocketManagerSpec, SocketParsable, SocketDat engine?.disconnect(reason: "Disconnect") } + /// Force disconnects the manager and all associated sockets. + open func close() { + DefaultSocketLogger.Logger.log("Manager closing", type: SocketManager.logType) + + status = .disconnected + + engine?.close(reason: "Disconnect") + } + + /// Disconnects the given socket. /// /// This will remove the socket for the manager's control, and make the socket instance useless and ready for @@ -565,7 +575,7 @@ open class SocketManager: NSObject, SocketManagerSpec, SocketParsable, SocketDat _config = config - if socketURL.absoluteString.hasPrefix("https://") { + if socketURL.absoluteString.hasPrefix("https://") || socketURL.absoluteString.hasPrefix("wss://") { _config.insert(.secure(true)) } diff --git a/Source/SocketIO/Manager/SocketManagerSpec.swift b/Source/SocketIO/Manager/SocketManagerSpec.swift index 8c57c91e..d1d27843 100644 --- a/Source/SocketIO/Manager/SocketManagerSpec.swift +++ b/Source/SocketIO/Manager/SocketManagerSpec.swift @@ -105,6 +105,9 @@ public protocol SocketManagerSpec : SocketEngineClient { /// Disconnects the manager and all associated sockets. func disconnect() + /// Force disconnects the manager and all associated sockets. + func close() + /// Disconnects the given socket. /// /// - parameter socket: The socket to disconnect.