Skip to content
67 changes: 64 additions & 3 deletions Source/SocketIO/Client/SocketIOClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -131,7 +150,6 @@ open class SocketIOClient: NSObject, SocketIOClientSpec {
}

status = .connecting

joinNamespace(withPayload: payload)

switch manager.version {
Expand Down Expand Up @@ -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

Expand All @@ -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!])
}

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions Source/SocketIO/Client/SocketIOClientSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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()

Expand Down
17 changes: 17 additions & 0 deletions Source/SocketIO/Engine/SocketEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions Source/SocketIO/Engine/SocketEngineSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
14 changes: 12 additions & 2 deletions Source/SocketIO/Manager/SocketManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}

Expand Down
3 changes: 3 additions & 0 deletions Source/SocketIO/Manager/SocketManagerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down