From ea8b66021ff11aee4a397f2be323242d833a98ae Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Thu, 4 Dec 2025 13:58:43 +0100 Subject: [PATCH 01/10] Add socketio connection state recovery --- Source/SocketIO/Client/SocketIOClient.swift | 29 ++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index d97bae1e..ee4a751a 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -77,6 +77,17 @@ 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? { + didSet { + recovered = pid == oldValue + } + } + + /// 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 let ackHandlers = SocketAckManager() var connectPayload: [String: Any]? @@ -130,9 +141,11 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { return } + let payloadWithConnectionStateRecovery = getConnectionStateRecoveryPayload(with: payload) + status = .connecting - joinNamespace(withPayload: payload) + joinNamespace(withPayload: payloadWithConnectionStateRecovery) switch manager.version { case .three: @@ -159,6 +172,14 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { } } + func getConnectionStateRecoveryPayload(with payload: [String: Any]?) -> [String: Any]? { + guard let pid, let lastEventOffset else { return payload } + var recoveryPayload = payload ?? [:] + recoveryPayload["pid"] = pid + recoveryPayload["offset"] = lastEventOffset + return recoveryPayload + } + func createOnAck(_ items: [Any], binary: Bool = true) -> OnAckCallback { currentAck += 1 @@ -175,6 +196,7 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { DefaultSocketLogger.Logger.log("Socket connected", type: logType) status = .connected + pid = payload?["pid"] as? String sid = payload?["sid"] as? String handleClientEvent(.connect, data: payload == nil ? [namespace] : [namespace, payload!]) @@ -359,6 +381,11 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { guard status == .connected || isInternalMessage else { return } + if let eventOffset = data.last as? String, + !isInternalMessage && ack < 0 && pid != nil { + self.lastEventOffset = eventOffset + } + DefaultSocketLogger.Logger.log("Handling event: \(event) with data: \(data)", type: logType) anyHandler?(SocketAnyEvent(event: event, items: data)) From 3b3de22b0ce9df35c85ee6b63b6b01379de4066d Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Thu, 4 Dec 2025 14:04:46 +0100 Subject: [PATCH 02/10] Fix recovered if pid is not sent --- Source/SocketIO/Client/SocketIOClient.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index ee4a751a..8fe8b3d4 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -80,7 +80,7 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { /// The id of this socket.io connect for connection state recovery. public private(set) var pid: String? { didSet { - recovered = pid == oldValue + recovered = pid != nil && pid == oldValue } } @@ -381,8 +381,8 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { guard status == .connected || isInternalMessage else { return } - if let eventOffset = data.last as? String, - !isInternalMessage && ack < 0 && pid != nil { + if !isInternalMessage && ack < 0 && pid != nil, + let eventOffset = data.last as? String { self.lastEventOffset = eventOffset } From 2a0a7730064dd5df96c94dd3d3b9cf05af6622c6 Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Fri, 5 Dec 2025 18:34:54 +0100 Subject: [PATCH 03/10] fix handle events when packets are sent before the socket is connected --- Source/SocketIO/Client/SocketIOClient.swift | 33 +++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index 8fe8b3d4..cac70c9d 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -89,6 +89,9 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { /// 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]? @@ -193,13 +196,15 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { open func didConnect(toNamespace namespace: String, payload: [String: Any]?) { guard status != .connected else { return } + pid = payload?["pid"] as? String + sid = payload?["sid"] as? String + DefaultSocketLogger.Logger.log("Socket connected", type: logType) status = .connected - pid = payload?["pid"] as? String - sid = payload?["sid"] as? String handleClientEvent(.connect, data: payload == nil ? [namespace] : [namespace, payload!]) + handleSavedEventPackets() } /// Called when the client has disconnected from socket.io. @@ -404,6 +409,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) @@ -416,6 +422,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) From 1b2247e3b11b95ba508b2e884c4a8904f69d50c4 Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Tue, 9 Dec 2025 15:22:24 +0100 Subject: [PATCH 04/10] fix recovered with didSet problem --- Source/SocketIO/Client/SocketIOClient.swift | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index cac70c9d..37a2d020 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -78,11 +78,7 @@ 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? { - didSet { - recovered = pid != nil && pid == oldValue - } - } + public private(set) var pid: String? /// Offset of last socket.io event for connection state recovery. public private(set) var lastEventOffset: String? @@ -195,8 +191,9 @@ 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 } - - pid = payload?["pid"] as? String + 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) @@ -386,11 +383,6 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { guard status == .connected || isInternalMessage else { return } - if !isInternalMessage && ack < 0 && pid != nil, - let eventOffset = data.last as? String { - self.lastEventOffset = eventOffset - } - DefaultSocketLogger.Logger.log("Handling event: \(event) with data: \(data)", type: logType) anyHandler?(SocketAnyEvent(event: event, items: data)) @@ -398,6 +390,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 From 047600e6def388a9a6f6f53524a6ba962188c4f5 Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Tue, 9 Dec 2025 15:52:49 +0100 Subject: [PATCH 05/10] feat engine add force close without message --- Source/SocketIO/Engine/SocketEngine.swift | 9 +++++++++ Source/SocketIO/Engine/SocketEngineSpec.swift | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/Source/SocketIO/Engine/SocketEngine.swift b/Source/SocketIO/Engine/SocketEngine.swift index 1d1c071d..2deafecb 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 foce closed.", type: SocketEngine.logType) + closeOutEngine(reason: reason) + } + private func _disconnect(reason: String) { guard connected && !closed else { return closeOutEngine(reason: reason) } 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. /// From 8db5692755052effc3de72f0771c474ce2ae70eb Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Tue, 9 Dec 2025 16:22:10 +0100 Subject: [PATCH 06/10] feat manager add force close without message --- Source/SocketIO/Manager/SocketManager.swift | 10 ++++++++++ Source/SocketIO/Manager/SocketManagerSpec.swift | 3 +++ 2 files changed, 13 insertions(+) diff --git a/Source/SocketIO/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift index d69aa11f..4311f413 100644 --- a/Source/SocketIO/Manager/SocketManager.swift +++ b/Source/SocketIO/Manager/SocketManager.swift @@ -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 diff --git a/Source/SocketIO/Manager/SocketManagerSpec.swift b/Source/SocketIO/Manager/SocketManagerSpec.swift index 8c57c91e..d91aa1f6 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() + /// Foce disconnects the manager and all associated sockets. + func close() + /// Disconnects the given socket. /// /// - parameter socket: The socket to disconnect. From 186807d9ffc5c20f81e3dfe785592b64c5ad5e97 Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Wed, 10 Dec 2025 12:40:03 +0100 Subject: [PATCH 07/10] fix conform changes with specs --- .../SocketIO/Client/SocketIOClientSpec.swift | 21 +++++++++++++++++++ Source/SocketIO/Engine/SocketEngine.swift | 2 +- .../SocketIO/Manager/SocketManagerSpec.swift | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) 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 2deafecb..28dd89a9 100644 --- a/Source/SocketIO/Engine/SocketEngine.swift +++ b/Source/SocketIO/Engine/SocketEngine.swift @@ -340,7 +340,7 @@ open class SocketEngine: NSObject, WebSocketDelegate, URLSessionDelegate, /// - 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 foce closed.", type: SocketEngine.logType) + DefaultSocketLogger.Logger.log("Engine is being force closed.", type: SocketEngine.logType) closeOutEngine(reason: reason) } diff --git a/Source/SocketIO/Manager/SocketManagerSpec.swift b/Source/SocketIO/Manager/SocketManagerSpec.swift index d91aa1f6..d1d27843 100644 --- a/Source/SocketIO/Manager/SocketManagerSpec.swift +++ b/Source/SocketIO/Manager/SocketManagerSpec.swift @@ -105,7 +105,7 @@ public protocol SocketManagerSpec : SocketEngineClient { /// Disconnects the manager and all associated sockets. func disconnect() - /// Foce disconnects the manager and all associated sockets. + /// Force disconnects the manager and all associated sockets. func close() /// Disconnects the given socket. From 6bbb308070cf60bbd0192636249f2ee8a9c54ead Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Thu, 11 Dec 2025 11:36:07 +0100 Subject: [PATCH 08/10] fix leak, wss links, errors from other pr --- Source/SocketIO/Engine/SocketEngine.swift | 8 ++++++++ Source/SocketIO/Manager/SocketManager.swift | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/SocketIO/Engine/SocketEngine.swift b/Source/SocketIO/Engine/SocketEngine.swift index 28dd89a9..3445fea9 100644 --- a/Source/SocketIO/Engine/SocketEngine.swift +++ b/Source/SocketIO/Engine/SocketEngine.swift @@ -572,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 @@ -723,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/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift index 4311f413..acaf92ae 100644 --- a/Source/SocketIO/Manager/SocketManager.swift +++ b/Source/SocketIO/Manager/SocketManager.swift @@ -575,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)) } From 2bcacae157b86d467d83356bac1701a4193215b6 Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Thu, 11 Dec 2025 18:32:37 +0100 Subject: [PATCH 09/10] feat recover allow nil offset like js implementation --- Source/SocketIO/Client/SocketIOClient.swift | 6 ++++-- Source/SocketIO/Manager/SocketManager.swift | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index 37a2d020..f98ec247 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -172,10 +172,12 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { } func getConnectionStateRecoveryPayload(with payload: [String: Any]?) -> [String: Any]? { - guard let pid, let lastEventOffset else { return payload } + guard let pid else { return payload } var recoveryPayload = payload ?? [:] recoveryPayload["pid"] = pid - recoveryPayload["offset"] = lastEventOffset + if let lastEventOffset { + recoveryPayload["offset"] = lastEventOffset + } return recoveryPayload } diff --git a/Source/SocketIO/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift index acaf92ae..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: From 27337bd3fb380ad7716a80c41abb4a1bc1561d06 Mon Sep 17 00:00:00 2001 From: Julian Ladjani Date: Fri, 12 Dec 2025 11:27:01 +0100 Subject: [PATCH 10/10] fix recover payload when network errors occurs --- Source/SocketIO/Client/SocketIOClient.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index f98ec247..a0ff9c55 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -89,7 +89,16 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { 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 @@ -140,11 +149,8 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { return } - let payloadWithConnectionStateRecovery = getConnectionStateRecoveryPayload(with: payload) - status = .connecting - - joinNamespace(withPayload: payloadWithConnectionStateRecovery) + joinNamespace(withPayload: payload) switch manager.version { case .three: @@ -202,8 +208,8 @@ open class SocketIOClient: NSObject, SocketIOClientSpec { status = .connected - handleClientEvent(.connect, data: payload == nil ? [namespace] : [namespace, payload!]) handleSavedEventPackets() + handleClientEvent(.connect, data: payload == nil ? [namespace] : [namespace, payload!]) } /// Called when the client has disconnected from socket.io.