diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a82e481 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# v13.0.0 + +What's new: +--- + +-Adds a new `SocketManager` class that multiplexes multiple namespaces through a single engine. +- Adds `.sentPing` and `.gotPong` client events for tracking ping/pongs. +- watchOS support. + +Important API changes +--- + +- Many properties that were previously on `SocketIOClient` have been moved to the `SocketManager`. +- `SocketIOClientOption.nsp` has been removed. Use `SocketManager.socket(forNamespace:)` to create/get a socket attached to a specific namespace. +- Adds `.sentPing` and `.gotPong` client events for tracking ping/pongs. +- Makes the framework a single target. +- Updates Starscream to 3.0 + diff --git a/README.md b/README.md index eb89fde..2bd4434 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,21 @@ Socket.IO-client for iOS/OS X. ```swift import SocketIO -let socket = SocketIOClient(socketURL: URL(string: "http://localhost:8080")!, config: [.log(true), .compress]) +let manager = SocketManager(socketURL: URL(string: "http://localhost:8080")!, config: [.log(true), .compress]) +let socket = manager.defaultSocket socket.on(clientEvent: .connect) {data, ack in print("socket connected") } socket.on("currentAmount") {data, ack in - if let cur = data[0] as? Double { - socket.emitWithAck("canUpdate", cur).timingOut(after: 0) {data in - socket.emit("update", ["amount": cur + 2.50]) - } - - ack.with("Got your currentAmount", "dude") + guard let cur = data[0] as? Double else { return } + + socket.emitWithAck("canUpdate", cur).timingOut(after: 0) {data in + socket.emit("update", ["amount": cur + 2.50]) } + + ack.with("Got your currentAmount", "dude") } socket.connect() @@ -29,8 +30,10 @@ socket.connect() ## Objective-C Example ```objective-c @import SocketIO; + NSURL* url = [[NSURL alloc] initWithString:@"http://localhost:8080"]; -SocketIOClient* socket = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"log": @YES, @"compress": @YES}]; +SocketManager* manager = [[SocketManager alloc] initWithSocketURL:url config:@{@"log": @YES, @"compress": @YES}]; +SocketIOClient* socket = manager.defaultSocket; [socket on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) { NSLog(@"socket connected"); @@ -134,6 +137,7 @@ Objective-C: # [Docs](https://nuclearace.github.io/Socket.IO-Client-Swift/index.html) - [Client](https://nuclearace.github.io/Socket.IO-Client-Swift/Classes/SocketIOClient.html) +- [Manager](https://nuclearace.github.io/Socket.IO-Client-Swift/Classes/SocketManager.html) - [Engine](https://nuclearace.github.io/Socket.IO-Client-Swift/Classes/SocketEngine.html) - [Options](https://nuclearace.github.io/Socket.IO-Client-Swift/Enums/SocketIOClientOption.html) diff --git a/Socket.IO-Client-Swift.xcodeproj/project.pbxproj b/Socket.IO-Client-Swift.xcodeproj/project.pbxproj index 496b154..32ecb6c 100644 --- a/Socket.IO-Client-Swift.xcodeproj/project.pbxproj +++ b/Socket.IO-Client-Swift.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1C6572803D7E252A77A86E5F /* SocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C65763817782DFAC67BE05C /* SocketManager.swift */; }; + 1C657FBB3F670261780FD72E /* SocketManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C6574AF9687A213814753E4 /* SocketManagerSpec.swift */; }; 1C686BE21F869AFD007D8627 /* SocketIOClientConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD21F869AF1007D8627 /* SocketIOClientConfigurationTest.swift */; }; 1C686BE31F869AFD007D8627 /* SocketEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD31F869AF1007D8627 /* SocketEngineTest.swift */; }; 1C686BE41F869AFD007D8627 /* SocketSideEffectTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD41F869AF1007D8627 /* SocketSideEffectTest.swift */; }; @@ -30,17 +32,18 @@ DD52B3A6C1E082841C35C85D /* SocketEngineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BE5FDCE1D684132E897C /* SocketEngineClient.swift */; }; DD52B44AE56F2E07F3F3F991 /* SocketAckManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B09F7984E730513AB7E5 /* SocketAckManager.swift */; }; DD52B4DFA12F2599410205D9 /* SocketEngineWebsocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BE9AD8B2BD7F841CD1D4 /* SocketEngineWebsocket.swift */; }; + DD52B53F2609D91A683DFCDD /* ManagerObjectiveCTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DD52BB5E907D283ACC31E17F /* ManagerObjectiveCTest.m */; }; DD52B56DE03CDB4F40BD1A23 /* SocketExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B471D780013E18DF9335 /* SocketExtensions.swift */; }; DD52B57E7ABC61B57EE2A4B8 /* SocketPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B59C11D3D2BC63612E50 /* SocketPacket.swift */; }; - DD52B660D63B6A25C3755AA7 /* SocketClientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B282975446C9A9C56D7B /* SocketClientManager.swift */; }; DD52B883F942CD5A9D29892B /* SocketEnginePollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B2D110F55723F82B108E /* SocketEnginePollable.swift */; }; DD52B9412F660F828B683422 /* SocketParsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B31D0E6815F5F10CEFB6 /* SocketParsable.swift */; }; DD52BB69B6D260035B652CA4 /* SocketAnyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B5A9DE10C7A8AD35617F /* SocketAnyEvent.swift */; }; DD52BB82239886CF6ADD642C /* SocketEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B7A9779A2E08075E5AAC /* SocketEngine.swift */; }; DD52BB9A3E42FF2DD6BE7C2F /* SocketIOClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */; }; DD52BC3F1F880820E8FDFD0C /* SocketLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BED81BF312B0E90E92AC /* SocketLogger.swift */; }; + DD52BCCD25EFA76E0F9B313C /* SocketMangerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BBAC5FAA7730D32CD5BF /* SocketMangerTest.swift */; }; DD52BD065B74AC5B77BAEFAA /* SocketIOClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B57FFEE8560CFFD793B3 /* SocketIOClientConfiguration.swift */; }; - DD52BE4D1E6BB752CD9614A6 /* SocketIOClientStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B1D9BC4AE46D38D827DE /* SocketIOClientStatus.swift */; }; + DD52BE4D1E6BB752CD9614A6 /* SocketIOStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B1D9BC4AE46D38D827DE /* SocketIOStatus.swift */; }; DD52BF924BEF05E1235CFD29 /* SocketIOClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BA1F41F2E4B3DC20260E /* SocketIOClient.swift */; }; DD52BFBC9E7CC32D3515AC80 /* SocketEngineSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B645273A873667BC2D43 /* SocketEngineSpec.swift */; }; DD52BFEB4DBD3BF8D93DAEFF /* SocketEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */; }; @@ -57,6 +60,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1C6574AF9687A213814753E4 /* SocketManagerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketManagerSpec.swift; sourceTree = ""; }; + 1C65763817782DFAC67BE05C /* SocketManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketManager.swift; sourceTree = ""; }; 1C686BD21F869AF1007D8627 /* SocketIOClientConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketIOClientConfigurationTest.swift; sourceTree = ""; }; 1C686BD31F869AF1007D8627 /* SocketEngineTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketEngineTest.swift; sourceTree = ""; }; 1C686BD41F869AF1007D8627 /* SocketSideEffectTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketSideEffectTest.swift; sourceTree = ""; }; @@ -83,8 +88,8 @@ 9432E00D1F77F889006AF628 /* Starscream.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Starscream.framework; path = Carthage/Build/tvOS/Starscream.framework; sourceTree = ""; }; DD52B078DB0A3C3D1BB507CD /* SocketIOClientOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientOption.swift; sourceTree = ""; }; DD52B09F7984E730513AB7E5 /* SocketAckManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketAckManager.swift; sourceTree = ""; }; - DD52B1D9BC4AE46D38D827DE /* SocketIOClientStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientStatus.swift; sourceTree = ""; }; - DD52B282975446C9A9C56D7B /* SocketClientManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketClientManager.swift; sourceTree = ""; }; + DD52B1D9BC4AE46D38D827DE /* SocketIOStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOStatus.swift; sourceTree = ""; }; + DD52B2C54A6ADF3371C13DCB /* SocketObjectiveCTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SocketObjectiveCTest.h; sourceTree = ""; }; DD52B2D110F55723F82B108E /* SocketEnginePollable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEnginePollable.swift; sourceTree = ""; }; DD52B31D0E6815F5F10CEFB6 /* SocketParsable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketParsable.swift; sourceTree = ""; }; DD52B471D780013E18DF9335 /* SocketExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketExtensions.swift; sourceTree = ""; }; @@ -95,8 +100,11 @@ DD52B645273A873667BC2D43 /* SocketEngineSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineSpec.swift; sourceTree = ""; }; DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEventHandler.swift; sourceTree = ""; }; DD52B7A9779A2E08075E5AAC /* SocketEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngine.swift; sourceTree = ""; }; + DD52B8396C7DEE7BFD6A985A /* ManagerObjectiveCTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ManagerObjectiveCTest.h; sourceTree = ""; }; DD52BA1F41F2E4B3DC20260E /* SocketIOClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClient.swift; sourceTree = ""; }; DD52BA240D139F72633D4159 /* SocketStringReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketStringReader.swift; sourceTree = ""; }; + DD52BB5E907D283ACC31E17F /* ManagerObjectiveCTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ManagerObjectiveCTest.m; sourceTree = ""; }; + DD52BBAC5FAA7730D32CD5BF /* SocketMangerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketMangerTest.swift; sourceTree = ""; }; DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientSpec.swift; sourceTree = ""; }; DD52BDC9E66AADA2CC5E8246 /* SocketTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketTypes.swift; sourceTree = ""; }; DD52BE5FDCE1D684132E897C /* SocketEngineClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineClient.swift; sourceTree = ""; }; @@ -128,6 +136,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1C657951DEA2E0293D0FD1B6 /* Manager */ = { + isa = PBXGroup; + children = ( + 1C65763817782DFAC67BE05C /* SocketManager.swift */, + 1C6574AF9687A213814753E4 /* SocketManagerSpec.swift */, + ); + name = Manager; + path = Source/SocketIO/Manager; + sourceTree = ""; + }; 1C686BD11F869AF1007D8627 /* TestSocketIO */ = { isa = PBXGroup; children = ( @@ -138,6 +156,7 @@ 1C686BD61F869AF1007D8627 /* SocketAckManagerTest.swift */, 1C686BD71F869AF1007D8627 /* SocketParserTest.swift */, 1C686BD81F869AF1007D8627 /* SocketNamespacePacketTest.swift */, + DD52BBAC5FAA7730D32CD5BF /* SocketMangerTest.swift */, ); name = TestSocketIO; path = Tests/TestSocketIO; @@ -147,6 +166,9 @@ isa = PBXGroup; children = ( 1C686BFE1F869E9D007D8627 /* SocketObjectiveCTest.m */, + DD52BB5E907D283ACC31E17F /* ManagerObjectiveCTest.m */, + DD52B8396C7DEE7BFD6A985A /* ManagerObjectiveCTest.h */, + DD52B2C54A6ADF3371C13DCB /* SocketObjectiveCTest.h */, ); name = TestSocketIOObjc; path = Tests/TestSocketIOObjc; @@ -198,6 +220,7 @@ DD52B6A0966AF71393777311 /* Client */, DD52B1D10D761CEF3944A6BC /* Util */, DD52B647ED881F3FF6EEC617 /* Parse */, + 1C657951DEA2E0293D0FD1B6 /* Manager */, ); name = Source; sourceTree = ""; @@ -251,7 +274,6 @@ DD52BED81BF312B0E90E92AC /* SocketLogger.swift */, DD52B471D780013E18DF9335 /* SocketExtensions.swift */, DD52BA240D139F72633D4159 /* SocketStringReader.swift */, - DD52B282975446C9A9C56D7B /* SocketClientManager.swift */, 9432E0061F77F7CA006AF628 /* SSLSecurity.swift */, ); name = Util; @@ -276,7 +298,7 @@ DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */, DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */, DD52B078DB0A3C3D1BB507CD /* SocketIOClientOption.swift */, - DD52B1D9BC4AE46D38D827DE /* SocketIOClientStatus.swift */, + DD52B1D9BC4AE46D38D827DE /* SocketIOStatus.swift */, DD52B57FFEE8560CFFD793B3 /* SocketIOClientConfiguration.swift */, ); name = Client; @@ -448,15 +470,16 @@ 9432E00F1F77F8C4006AF628 /* SSLSecurity.swift in Sources */, DD52BB9A3E42FF2DD6BE7C2F /* SocketIOClientSpec.swift in Sources */, DD52B2AFE7D46039C7AE4D19 /* SocketIOClientOption.swift in Sources */, - DD52BE4D1E6BB752CD9614A6 /* SocketIOClientStatus.swift in Sources */, + DD52BE4D1E6BB752CD9614A6 /* SocketIOStatus.swift in Sources */, DD52BD065B74AC5B77BAEFAA /* SocketIOClientConfiguration.swift in Sources */, DD52B048C71D724ABBD18C71 /* SocketTypes.swift in Sources */, DD52BC3F1F880820E8FDFD0C /* SocketLogger.swift in Sources */, DD52B56DE03CDB4F40BD1A23 /* SocketExtensions.swift in Sources */, DD52B11AF936352BAE30B2C8 /* SocketStringReader.swift in Sources */, - DD52B660D63B6A25C3755AA7 /* SocketClientManager.swift in Sources */, DD52B57E7ABC61B57EE2A4B8 /* SocketPacket.swift in Sources */, DD52B9412F660F828B683422 /* SocketParsable.swift in Sources */, + 1C6572803D7E252A77A86E5F /* SocketManager.swift in Sources */, + 1C657FBB3F670261780FD72E /* SocketManagerSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -472,6 +495,8 @@ 1C686BE61F869AFD007D8627 /* SocketAckManagerTest.swift in Sources */, 1C686BE71F869AFD007D8627 /* SocketParserTest.swift in Sources */, 1C686BE81F869AFD007D8627 /* SocketNamespacePacketTest.swift in Sources */, + DD52BCCD25EFA76E0F9B313C /* SocketMangerTest.swift in Sources */, + DD52B53F2609D91A683DFCDD /* ManagerObjectiveCTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Source/SocketIO/Ack/SocketAckEmitter.swift b/Source/SocketIO/Ack/SocketAckEmitter.swift index 5776289..db8cecf 100644 --- a/Source/SocketIO/Ack/SocketAckEmitter.swift +++ b/Source/SocketIO/Ack/SocketAckEmitter.swift @@ -120,8 +120,10 @@ public final class OnAckCallback : NSObject { guard seconds != 0 else { return } - socket.handleQueue.asyncAfter(deadline: DispatchTime.now() + seconds) { - socket.ackHandlers.timeoutAck(self.ackNumber, onQueue: socket.handleQueue) + socket.manager?.handleQueue.asyncAfter(deadline: DispatchTime.now() + seconds) {[weak socket] in + guard let socket = socket, let manager = socket.manager else { return } + + socket.ackHandlers.timeoutAck(self.ackNumber, onQueue: manager.handleQueue) } } diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index 697f274..20b83f2 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -25,110 +25,52 @@ import Dispatch import Foundation -/// The main class for SocketIOClientSwift. +/// Represents a socket.io-client. /// -/// **NOTE**: The client is not thread/queue safe, all interaction with the socket should be done on the `handleQueue` +/// Clients are created through a `SocketManager`, which owns the `SocketEngineSpec` that controls the connection to the server. /// -/// Represents a socket.io-client. Most interaction with socket.io will be through this class. -open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, SocketParsable { +/// For example: +/// +/// ```swift +/// // Create a socket for the /swift namespace +/// let socket = manager.socket(forNamespace: "/swift") +/// +/// // Add some handlers and connect +/// ``` +/// +/// **NOTE**: The client is not thread/queue safe, all interaction with the socket should be done on the `manager.handleQueue` +/// +open class SocketIOClient : NSObject, SocketIOClientSpec { // MARK: Properties - private static let logType = "SocketIOClient" - - /// If `true` then every time `connect` is called, a new engine will be created. - @objc - public var forceNew = false - - /// The queue that all interaction with the client should occur on. This is the queue that event handlers are - /// called on. - /// - /// **This should be a serial queue! Concurrent queues are not supported and might cause crashes and races**. - @objc - public var handleQueue = DispatchQueue.main - /// The namespace that this socket is currently connected to. /// /// **Must** start with a `/`. @objc public var nsp = "/" - /// The configuration for this client. - /// - /// **This cannot be set after calling one of the connect methods**. - public var config: SocketIOClientConfiguration { - get { - return _config - } - - set { - guard status == .notConnected else { - DefaultSocketLogger.Logger.error("Tried setting config after calling connect", - type: SocketIOClient.logType) - return - } - - _config = newValue - - if socketURL.absoluteString.hasPrefix("https://") { - _config.insert(.secure(true)) - } - - _config.insert(.path("/socket.io/"), replacing: false) - setConfigs() - } - } - - /// If `true`, this client will try and reconnect on any disconnects. - @objc - public var reconnects = true - - /// The number of seconds to wait before attempting to reconnect. - @objc - public var reconnectWait = 10 - /// The session id of this client. @objc - public var sid: String? { - return engine?.sid + public var sid: String { + guard let engine = manager?.engine else { return "" } + + return nsp == "/" ? engine.sid : "\(nsp)#\(engine.sid)" } - /// The URL of the socket.io server. - /// - /// If changed after calling `init`, `forceNew` must be set to `true`, or it will only connect to the url set in the - /// init. - @objc - public var socketURL: URL - - /// 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.** - 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 internal(set) var engine: SocketEngineSpec? - /// The array of handlers for this socket. public private(set) var handlers = [SocketEventHandler]() + /// The manager for this socket. + @objc + public private(set) weak var manager: SocketManagerSpec? + /// The status of this client. @objc - public private(set) var status = SocketIOClientStatus.notConnected { + public private(set) var status = SocketIOStatus.notConnected { didSet { - switch status { - case .connected: - reconnecting = false - currentReconnectAttempt = 0 - default: - break - } - handleClientEvent(.statusChange, data: [status]) } } @@ -136,60 +78,29 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So var ackHandlers = SocketAckManager() private(set) var currentAck = -1 - private(set) var reconnectAttempts = -1 - private var _config: SocketIOClientConfiguration - private var currentReconnectAttempt = 0 - private var reconnecting = false + private lazy var logType = "SocketIOClient{\(nsp)}" // MARK: Initializers /// Type safe way to create a new SocketIOClient. `opts` can be omitted. /// + /// - parameter manager: The manager for this socket. /// - parameter socketURL: The url of the socket.io server. - /// - parameter config: The config for this socket. - public init(socketURL: URL, config: SocketIOClientConfiguration = []) { - self._config = config - self.socketURL = socketURL - - if socketURL.absoluteString.hasPrefix("https://") { - self._config.insert(.secure(true)) - } - - self._config.insert(.path("/socket.io/"), replacing: false) + @objc + public init(manager: SocketManagerSpec, nsp: String) { + self.manager = manager + self.nsp = nsp super.init() - - setConfigs() - } - - /// Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity. - /// If using Swift it's recommended to use `init(socketURL: NSURL, options: Set)` - /// - /// - parameter socketURL: The url of the socket.io server. - /// - parameter config: The config for this socket. - @objc - public convenience init(socketURL: NSURL, config: NSDictionary?) { - self.init(socketURL: socketURL as URL, config: config?.toSocketConfiguration() ?? []) } deinit { - DefaultSocketLogger.Logger.log("Client is being released", type: SocketIOClient.logType) - engine?.disconnect(reason: "Client Deinit") + DefaultSocketLogger.Logger.log("Client is being released", type: logType) } // MARK: Methods - private func addEngine() { - DefaultSocketLogger.Logger.log("Adding engine", type: SocketIOClient.logType) - - engine?.engineQueue.sync { - self.engine?.client = nil - } - - engine = SocketEngine(client: self, url: socketURL, config: config) - } - /// Connect to the server. The same as calling `connect(timeoutAfter:withHandler:)` with a timeout of 0. /// /// Only call after adding your event listeners, unless you know what you're doing. @@ -209,27 +120,22 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So open func connect(timeoutAfter: Double, withHandler handler: (() -> ())?) { assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)") - guard status != .connected else { - DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", - type: SocketIOClient.logType) + guard let manager = self.manager, status != .connected else { + DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", type: logType) return } status = .connecting - if engine == nil || forceNew { - addEngine() - } - - engine?.connect() + manager.connectSocket(self) guard timeoutAfter != 0 else { return } - handleQueue.asyncAfter(deadline: DispatchTime.now() + timeoutAfter) {[weak self] in + manager.handleQueue.asyncAfter(deadline: DispatchTime.now() + timeoutAfter) {[weak self] in guard let this = self, this.status == .connecting || this.status == .notConnected else { return } this.status = .disconnected - this.engine?.disconnect(reason: "Connect timeout") + this.leaveNamespace() handler?() } @@ -248,7 +154,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So open func didConnect(toNamespace namespace: String) { guard status != .connected else { return } - DefaultSocketLogger.Logger.log("Socket connected", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Socket connected", type: logType) status = .connected @@ -261,21 +167,22 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So open func didDisconnect(reason: String) { guard status != .disconnected else { return } - DefaultSocketLogger.Logger.log("Disconnected: \(reason)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Disconnected: \(reason)", type: logType) - reconnecting = false status = .disconnected - // Make sure the engine is actually dead. - engine?.disconnect(reason: reason) handleClientEvent(.disconnect, data: [reason]) } /// Disconnects the socket. + /// + /// This will cause the socket to leave the namespace it is associated to, as well as remove itself from the + /// `manager`. @objc open func disconnect() { - DefaultSocketLogger.Logger.log("Closing socket", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Closing socket", type: logType) + leaveNamespace() didDisconnect(reason: "Disconnect") } @@ -291,7 +198,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So try emit(event, with: items.map({ try $0.socketRepresentation() })) } catch let err { DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)", - type: SocketIOClient.logType) + type: logType) handleClientEvent(.error, data: [event, items, err]) } @@ -335,7 +242,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So return emitWithAck(event, with: try items.map({ try $0.socketRepresentation() })) } catch let err { DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)", - type: SocketIOClient.logType) + type: logType) handleClientEvent(.error, data: [event, items, err]) @@ -373,9 +280,9 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So let packet = SocketPacket.packetFromEmit(data, id: ack ?? -1, nsp: nsp, ack: false) let str = packet.packetString - DefaultSocketLogger.Logger.log("Emitting: \(str)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Emitting: \(str)", type: logType) - engine?.send(str, withData: packet.binary) + manager?.engine?.send(str, withData: packet.binary) } /// Call when you wish to tell the server that you've received the event for `ack`. @@ -390,91 +297,9 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So let packet = SocketPacket.packetFromEmit(items, id: ack, nsp: nsp, ack: true) let str = packet.packetString - DefaultSocketLogger.Logger.log("Emitting Ack: \(str)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Emitting Ack: \(str)", type: logType) - engine?.send(str, withData: packet.binary) - } - - /// Called when the engine closes. - /// - /// - parameter reason: The reason that the engine closed. - open func engineDidClose(reason: String) { - handleQueue.async { - self._engineDidClose(reason: reason) - } - } - - private func _engineDidClose(reason: String) { - waitingPackets.removeAll() - - if status != .disconnected { - status = .notConnected - } - - if status == .disconnected || !reconnects { - didDisconnect(reason: reason) - } else if !reconnecting { - reconnecting = true - tryReconnect(reason: reason) - } - } - - /// Called when the engine errors. - /// - /// - parameter reason: The reason the engine errored. - open func engineDidError(reason: String) { - handleQueue.async { - self._engineDidError(reason: reason) - } - } - - private func _engineDidError(reason: String) { - DefaultSocketLogger.Logger.error("\(reason)", type: SocketIOClient.logType) - - handleClientEvent(.error, data: [reason]) - } - - /// Called when the engine opens. - /// - /// - parameter reason: The reason the engine opened. - open func engineDidOpen(reason: String) { - handleQueue.async { - self._engineDidOpen(reason: reason) - } - } - - private func _engineDidOpen(reason: String) { - DefaultSocketLogger.Logger.log("Engine opened \(reason)", type: SocketIOClient.logType) - - guard nsp != "/" else { - didConnect(toNamespace: "/") - - return - } - - joinNamespace(nsp) - } - - /// Called when the engine receives a pong message. - open func engineDidReceivePong() { - handleQueue.async { - self._engineDidReceivePong() - } - } - - private func _engineDidReceivePong() { - handleClientEvent(.pong, data: []) - } - - /// Called when the sends a ping to the server. - open func engineDidSendPing() { - handleQueue.async { - self._engineDidSendPing() - } - } - - private func _engineDidSendPing() { - handleClientEvent(.ping, data: []) + manager?.engine?.send(str, withData: packet.binary) } /// Called when socket.io has acked one of our emits. Causes the corresponding ack callback to be called. @@ -483,11 +308,19 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So /// - parameter data: The data sent back with this ack. @objc open func handleAck(_ ack: Int, data: [Any]) { - guard status == .connected else { return } + guard status == .connected, let manager = self.manager else { return } - DefaultSocketLogger.Logger.log("Handling ack: \(ack) with data: \(data)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Handling ack: \(ack) with data: \(data)", type: logType) - ackHandlers.executeAck(ack, with: data, onQueue: handleQueue) + ackHandlers.executeAck(ack, with: data, onQueue: manager.handleQueue) + } + + /// 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) } /// Called when we get an event from socket.io. @@ -500,7 +333,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { guard status == .connected || isInternalMessage else { return } - DefaultSocketLogger.Logger.log("Handling event: \(event) with data: \(data)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Handling event: \(event) with data: \(data)", type: logType) anyHandler?(SocketAnyEvent(event: event, items: data)) @@ -509,36 +342,49 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So } } - /// Called on socket.io specific events. + /// Causes a client to handle a socket.io packet. The namespace for the packet must match the namespace of the + /// socket. /// - /// - 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) + /// - parameter pack: The packet to handle. + open func handlePacket(_ pack: SocketPacket) { + func handleConnect(_ packetNamespace: String) { + guard packetNamespace == nsp else { return } + + didConnect(toNamespace: packetNamespace) + } + + switch pack.type { + case .event, .binaryEvent: + handleEvent(pack.event, data: pack.args, isInternalMessage: false, withAck: pack.id) + case .ack, .binaryAck: + handleAck(pack.id, data: pack.data) + case .connect: + handleConnect(pack.nsp) + case .disconnect: + didDisconnect(reason: "Got Disconnect") + case .error: + handleEvent("error", data: pack.data, isInternalMessage: true, withAck: pack.id) + } } - /// Call when you wish to leave a namespace and return to the default namespace. + /// Call when you wish to leave a namespace and disconnect this socket. @objc open func leaveNamespace() { guard nsp != "/" else { return } - engine?.send("1\(nsp)", withData: []) - nsp = "/" + status = .disconnected + + manager?.disconnectSocket(self) } - /// Joins `namespace`. - /// - /// **Do not use this to join the default namespace.** Instead call `leaveNamespace`. - /// - /// - parameter namespace: The namespace to join. + /// Joins `nsp`. @objc - open func joinNamespace(_ namespace: String) { - guard namespace != "/" else { return } + open func joinNamespace() { + guard nsp != "/" else { return } - DefaultSocketLogger.Logger.log("Joining namespace \(namespace)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Joining namespace \(nsp)", type: logType) - nsp = namespace - engine?.send("0\(nsp)", withData: []) + manager?.engine?.send("0\(nsp)", withData: []) } /// Removes handler(s) for a client event. @@ -557,7 +403,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So /// - parameter event: The event to remove handlers for. @objc open func off(_ event: String) { - DefaultSocketLogger.Logger.log("Removing handler for event: \(event)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Removing handler for event: \(event)", type: logType) handlers = handlers.filter({ $0.event != event }) } @@ -569,7 +415,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So /// - parameter id: The UUID of the handler you wish to remove. @objc open func off(id: UUID) { - DefaultSocketLogger.Logger.log("Removing handler with id: \(id)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Removing handler with id: \(id)", type: logType) handlers = handlers.filter({ $0.id != id }) } @@ -582,7 +428,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So @objc @discardableResult open func on(_ event: String, callback: @escaping NormalCallback) -> UUID { - DefaultSocketLogger.Logger.log("Adding handler for event: \(event)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Adding handler for event: \(event)", type: logType) let handler = SocketEventHandler(event: event, id: UUID(), callback: callback) handlers.append(handler) @@ -626,7 +472,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So @objc @discardableResult open func once(_ event: String, callback: @escaping NormalCallback) -> UUID { - DefaultSocketLogger.Logger.log("Adding once handler for event: \(event)", type: SocketIOClient.logType) + DefaultSocketLogger.Logger.log("Adding once handler for event: \(event)", type: logType) let id = UUID() @@ -649,31 +495,10 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So anyHandler = handler } - /// Called when the engine has a message that must be parsed. - /// - /// - parameter msg: The message that needs parsing. - public func parseEngineMessage(_ msg: String) { - DefaultSocketLogger.Logger.log("Should parse message: \(msg)", type: SocketIOClient.logType) - - handleQueue.async { self.parseSocketMessage(msg) } - } - - /// Called when the engine receives binary data. - /// - /// - parameter data: The data the engine received. - public func parseEngineBinaryData(_ data: Data) { - handleQueue.async { self.parseBinaryData(data) } - } - /// Tries to reconnect to the server. - /// - /// This will cause a `disconnect` event to be emitted, as well as an `reconnectAttempt` event. @objc - open func reconnect() { - guard !reconnecting else { return } - - engine?.disconnect(reason: "manual reconnect") - } + @available(*, unavailable, message: "Call the manager's reconnect method") + open func reconnect() { } /// Removes all handlers. /// @@ -683,54 +508,15 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So handlers.removeAll(keepingCapacity: false) } - private func tryReconnect(reason: String) { - guard reconnecting else { return } + /// Puts the socket back into the connecting state. + /// Called when the manager detects a broken connection, or when a manual reconnect is triggered. + /// + /// - parameter reason: The reason this socket is reconnecting. + @objc + open func setReconnecting(reason: String) { + status = .connecting - DefaultSocketLogger.Logger.log("Starting reconnect", type: SocketIOClient.logType) handleClientEvent(.reconnect, data: [reason]) - - _tryReconnect() - } - - private func _tryReconnect() { - guard reconnects && reconnecting && status != .disconnected else { return } - - if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts { - return didDisconnect(reason: "Reconnect Failed") - } - - DefaultSocketLogger.Logger.log("Trying to reconnect", type: SocketIOClient.logType) - handleClientEvent(.reconnectAttempt, data: [(reconnectAttempts - currentReconnectAttempt)]) - - currentReconnectAttempt += 1 - connect() - - handleQueue.asyncAfter(deadline: DispatchTime.now() + Double(reconnectWait), execute: _tryReconnect) - } - - private func setConfigs() { - for option in config { - switch option { - case let .reconnects(reconnects): - self.reconnects = reconnects - case let .reconnectAttempts(attempts): - reconnectAttempts = attempts - case let .reconnectWait(wait): - reconnectWait = abs(wait) - case let .nsp(nsp): - self.nsp = nsp - case let .log(log): - DefaultSocketLogger.Logger.log = log - case let .logger(logger): - DefaultSocketLogger.Logger = logger - case let .handleQueue(queue): - handleQueue = queue - case let .forceNew(force): - forceNew = force - default: - continue - } - } } // Test properties @@ -743,14 +529,10 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So status = .connected } - func setTestStatus(_ status: SocketIOClientStatus) { + func setTestStatus(_ status: SocketIOStatus) { self.status = status } - func setTestEngine(_ engine: SocketEngineSpec?) { - self.engine = engine - } - func emitTest(event: String, _ data: Any...) { emit([event] + data) } diff --git a/Source/SocketIO/Client/SocketIOClientConfiguration.swift b/Source/SocketIO/Client/SocketIOClientConfiguration.swift index 290dd5b..97f6d91 100644 --- a/Source/SocketIO/Client/SocketIOClientConfiguration.swift +++ b/Source/SocketIO/Client/SocketIOClientConfiguration.swift @@ -125,5 +125,12 @@ public struct SocketIOClientConfiguration : ExpressibleByArrayLiteral, Collectio backingArray.append(element) } - +} + +/// Declares that a type can set configs from a `SocketIOClientConfiguration`. +public protocol ConfigSettable { + /// Called when an `ConfigSettable` should set/update its configs from a given configuration. + /// + /// - parameter config: The `SocketIOClientConfiguration` that should be used to set/update configs. + mutating func setConfigs(_ config: SocketIOClientConfiguration) } diff --git a/Source/SocketIO/Client/SocketIOClientOption.swift b/Source/SocketIO/Client/SocketIOClientOption.swift index 10b521e..3e9114f 100644 --- a/Source/SocketIO/Client/SocketIOClientOption.swift +++ b/Source/SocketIO/Client/SocketIOClientOption.swift @@ -40,10 +40,6 @@ public enum SocketIOClientOption : ClientOption { /// An array of cookies that will be sent during the initial connection. case cookies([HTTPCookie]) - /// Deprecated - @available(*, deprecated, message: "No longer needed in socket.io 2.0+") - case doubleEncodeUTF8(Bool) - /// Any extra HTTP headers that should be sent during the initial connection. case extraHeaders([String: String]) @@ -67,10 +63,6 @@ public enum SocketIOClientOption : ClientOption { /// Used to pass in a custom logger. case logger(SocketLogger) - /// The namespace that this client should connect to. Can be changed during use using the `joinNamespace` - /// and `leaveNamespace` methods on `SocketIOClient`. - case nsp(String) - /// A custom path to socket.io. Only use this if the socket.io server is configured to look for this path. case path(String) @@ -96,11 +88,6 @@ public enum SocketIOClientOption : ClientOption { /// Sets an NSURLSessionDelegate for the underlying engine. Useful if you need to handle self-signed certs. case sessionDelegate(URLSessionDelegate) - /// If passed `true`, the WebSocket transport will try and use voip logic to keep network connections open in - /// the background. **This option is experimental as socket.io shouldn't be used for background communication.** - @available(*, deprecated, message: "No longer has any effect, and will be removed in v11.0") - case voipEnabled(Bool) - // MARK: Properties /// The description of this option. @@ -114,8 +101,6 @@ public enum SocketIOClientOption : ClientOption { description = "connectParams" case .cookies: description = "cookies" - case .doubleEncodeUTF8: - description = "doubleEncodeUTF8" case .extraHeaders: description = "extraHeaders" case .forceNew: @@ -130,8 +115,6 @@ public enum SocketIOClientOption : ClientOption { description = "log" case .logger: description = "logger" - case .nsp: - description = "nsp" case .path: description = "path" case .reconnects: @@ -148,8 +131,6 @@ public enum SocketIOClientOption : ClientOption { description = "security" case .sessionDelegate: description = "sessionDelegate" - case .voipEnabled: - description = "voipEnabled" } return description @@ -165,8 +146,6 @@ public enum SocketIOClientOption : ClientOption { value = params case let .cookies(cookies): value = cookies - case let .doubleEncodeUTF8(encode): - value = encode case let .extraHeaders(headers): value = headers case let .forceNew(force): @@ -181,8 +160,6 @@ public enum SocketIOClientOption : ClientOption { value = log case let .logger(logger): value = logger - case let .nsp(nsp): - value = nsp case let .path(path): value = path case let .reconnects(reconnects): @@ -199,8 +176,6 @@ public enum SocketIOClientOption : ClientOption { value = signed case let .sessionDelegate(delegate): value = delegate - case let .voipEnabled(enabled): - value = enabled } return value diff --git a/Source/SocketIO/Client/SocketIOClientSpec.swift b/Source/SocketIO/Client/SocketIOClientSpec.swift index 7038f7c..950f8aa 100644 --- a/Source/SocketIO/Client/SocketIOClientSpec.swift +++ b/Source/SocketIO/Client/SocketIOClientSpec.swift @@ -32,22 +32,19 @@ public protocol SocketIOClientSpec : class { /// A handler that will be called on any event. var anyHandler: ((SocketAnyEvent) -> ())? { get } - /// The configuration for this client. - var config: SocketIOClientConfiguration { get set } - - /// 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 manager for this socket. + var manager: SocketManagerSpec? { get } + /// The namespace that this socket is currently connected to. /// /// **Must** start with a `/`. var nsp: String { get set } /// The status of this client. - var status: SocketIOClientStatus { get } + var status: SocketIOStatus { get } // MARK: Methods @@ -126,6 +123,12 @@ public protocol SocketIOClientSpec : class { /// - parameter data: The data sent back with this ack. func handleAck(_ ack: Int, data: [Any]) + /// Called on socket.io specific events. + /// + /// - parameter event: The `SocketClientEvent`. + /// - parameter data: The data for this event. + func handleClientEvent(_ event: SocketClientEvent, data: [Any]) + /// Called when we get an event from socket.io. /// /// - parameter event: The name of the event. @@ -134,21 +137,17 @@ public protocol SocketIOClientSpec : class { /// - 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 specific events. + /// Causes a client to handle a socket.io packet. The namespace for the packet must match the namespace of the + /// socket. /// - /// - parameter event: The `SocketClientEvent`. - /// - parameter data: The data for this event. - func handleClientEvent(_ event: SocketClientEvent, data: [Any]) + /// - parameter pack: The packet to handle. + func handlePacket(_ pack: SocketPacket) - /// Call when you wish to leave a namespace and return to the default namespace. + /// Call when you wish to leave a namespace and disconnect this socket. func leaveNamespace() - /// Joins `namespace`. - /// - /// **Do not use this to join the default namespace.** Instead call `leaveNamespace`. - /// - /// - parameter namespace: The namespace to join. - func joinNamespace(_ namespace: String) + /// Joins `nsp`. + func joinNamespace() /// Removes handler(s) for a client event. /// @@ -212,13 +211,16 @@ public protocol SocketIOClientSpec : class { /// - 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() + + /// Puts the socket back into the connecting state. + /// Called when the manager detects a broken connection, or when a manual reconnect is triggered. + /// + /// parameter reason: The reason this socket is going reconnecting. + func setReconnecting(reason: String) } public extension SocketIOClientSpec { diff --git a/Source/SocketIO/Client/SocketIOClientStatus.swift b/Source/SocketIO/Client/SocketIOStatus.swift similarity index 58% rename from Source/SocketIO/Client/SocketIOClientStatus.swift rename to Source/SocketIO/Client/SocketIOStatus.swift index 8caacbc..4f298a2 100644 --- a/Source/SocketIO/Client/SocketIOClientStatus.swift +++ b/Source/SocketIO/Client/SocketIOStatus.swift @@ -1,5 +1,5 @@ // -// SocketIOClientStatus.swift +// SocketIOStatus.swift // Socket.IO-Client-Swift // // Created by Erik Little on 8/14/15. @@ -24,17 +24,36 @@ import Foundation -/// Represents the state of the client. -@objc public enum SocketIOClientStatus : Int { - /// The client has never been connected. Or the client has been reset. +/// Represents state of a manager or client. +@objc +public enum SocketIOStatus : Int, CustomStringConvertible { + // MARK: Cases + + /// The client/manager has never been connected. Or the client has been reset. case notConnected - /// The client was once connected, but not anymore. + /// The client/manager was once connected, but not anymore. case disconnected - /// The client is in the process of connecting. + /// The client/manager is in the process of connecting. case connecting - /// The client is currently connected. + /// The client/manager is currently connected. case connected + + // MARK: Properties + + /// - returns: True if this client/manager is connected/connecting to a server. + public var active: Bool { + return self == .connected || self == .connecting + } + + public var description: String { + switch self { + case .connected: return "connected" + case .connecting: return "connecting" + case .disconnected: return "disconnected" + case .notConnected: return "notConnected" + } + } } diff --git a/Source/SocketIO/Engine/SocketEngine.swift b/Source/SocketIO/Engine/SocketEngine.swift index d83bc9e..22ba793 100644 --- a/Source/SocketIO/Engine/SocketEngine.swift +++ b/Source/SocketIO/Engine/SocketEngine.swift @@ -28,7 +28,8 @@ import Starscream /// The class that handles the engine.io protocol and transports. /// See `SocketEnginePollable` and `SocketEngineWebsocket` for transport specific methods. -public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket { +public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket, + ConfigSettable { // MARK: Properties private static let logType = "SocketEngine" @@ -147,41 +148,11 @@ public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePoll public init(client: SocketEngineClient, url: URL, config: SocketIOClientConfiguration) { self.client = client self.url = url - for option in config { - switch option { - case let .connectParams(params): - connectParams = params - case let .cookies(cookies): - self.cookies = cookies - case let .extraHeaders(headers): - extraHeaders = headers - case let .sessionDelegate(delegate): - sessionDelegate = delegate - case let .forcePolling(force): - forcePolling = force - case let .forceWebsockets(force): - forceWebsockets = force - case let .path(path): - socketPath = path - - if !socketPath.hasSuffix("/") { - socketPath += "/" - } - case let .secure(secure): - self.secure = secure - case let .selfSigned(selfSigned): - self.selfSigned = selfSigned - case let .security(security): - self.security = security - case .compress: - self.compress = true - default: - continue - } - } super.init() + setConfigs(config) + sessionDelegate = sessionDelegate ?? self (urlPolling, urlWebSocket) = createURLs() @@ -572,6 +543,44 @@ public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePoll client?.engineDidSendPing() } + /// Called when the engine should set/update its configs from a given configuration. + /// + /// parameter config: The `SocketIOClientConfiguration` that should be used to set/update configs. + open func setConfigs(_ config: SocketIOClientConfiguration) { + for option in config { + switch option { + case let .connectParams(params): + connectParams = params + case let .cookies(cookies): + self.cookies = cookies + case let .extraHeaders(headers): + extraHeaders = headers + case let .sessionDelegate(delegate): + sessionDelegate = delegate + case let .forcePolling(force): + forcePolling = force + case let .forceWebsockets(force): + forceWebsockets = force + case let .path(path): + socketPath = path + + if !socketPath.hasSuffix("/") { + socketPath += "/" + } + case let .secure(secure): + self.secure = secure + case let .selfSigned(selfSigned): + self.selfSigned = selfSigned + case let .security(security): + self.security = security + case .compress: + self.compress = true + default: + continue + } + } + } + // Moves from long-polling to websockets private func upgradeTransport() { if ws?.isConnected ?? false { @@ -645,6 +654,12 @@ public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePoll client?.engineDidClose(reason: "Socket Disconnected") } } + + // Test Properties + + func setConnected(_ value: Bool) { + connected = value + } } extension SocketEngine { diff --git a/Source/SocketIO/Engine/SocketEngineSpec.swift b/Source/SocketIO/Engine/SocketEngineSpec.swift index 4dba805..c14c3f5 100644 --- a/Source/SocketIO/Engine/SocketEngineSpec.swift +++ b/Source/SocketIO/Engine/SocketEngineSpec.swift @@ -34,6 +34,9 @@ import Starscream /// `true` if this engine is closed. var closed: Bool { get } + /// If `true` the engine will attempt to use WebSocket compression. + var compress: Bool { get } + /// `true` if this engine is connected. Connected means that the initial poll connect has succeeded. var connected: Bool { get } diff --git a/Source/SocketIO/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift new file mode 100644 index 0000000..8009001 --- /dev/null +++ b/Source/SocketIO/Manager/SocketManager.swift @@ -0,0 +1,519 @@ +// +// Created by Erik Little on 10/14/17. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Dispatch +import Foundation + +/// +/// A manager for a socket.io connection. +/// +/// A `SocketManager` is responsible for multiplexing multiple namespaces through a single `SocketEngineSpec`. +/// +/// Example: +/// +/// ```swift +/// let manager = SocketManager(socketURL: URL(string:"http://localhost:8080/")!) +/// let defaultNamespaceSocket = manager.defaultSocket +/// let swiftSocket = manager.socket(forNamespace: "/swift") +/// +/// // defaultNamespaceSocket and swiftSocket both share a single connection to the server +/// ``` +/// +/// Sockets created through the manager are retained by the manager. So at the very least, a single strong reference +/// to the manager must be maintained to keep sockets alive. +/// +/// To disconnect a socket and remove it from the manager, either call `SocketIOClient.disconnect()` on the socket, +/// or call one of the `disconnectSocket` methods on this class. +/// +open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDataBufferable, ConfigSettable { + private static let logType = "SocketManager" + + // MARK Properties + + /// The socket associated with the default namespace ("/"). + public var defaultSocket: SocketIOClient? { + return socket(forNamespace: "/") + } + + /// The URL of the socket.io server. + /// + /// If changed after calling `init`, `forceNew` must be set to `true`, or it will only connect to the url set in the + /// init. + public let socketURL: URL + + /// The configuration for this client. + /// + /// **Some configs will not take affect until after a reconnect if set after calling a connect method**. + public var config: SocketIOClientConfiguration { + get { + return _config + } + + set { + if status.active { + DefaultSocketLogger.Logger.log("Setting configs on active manager. Some configs may not be applied until reconnect", + type: SocketManager.logType) + } + + setConfigs(newValue) + } + } + + /// The engine for this manager. + public var engine: SocketEngineSpec? + + /// If `true` then every time `connect` is called, a new engine will be created. + public var forceNew = false + + /// The queue that all interaction with the client should occur on. This is the queue that event handlers are + /// called on. + /// + /// **This should be a serial queue! Concurrent queues are not supported and might cause crashes and races**. + public var handleQueue = DispatchQueue.main + + /// The sockets in this manager indexed by namespace. + public var nsps = [String: SocketIOClient]() + + /// If `true`, this client will try and reconnect on any disconnects. + public var reconnects = true + + /// The number of seconds to wait before attempting to reconnect. + public var reconnectWait = 10 + + /// The status of this manager. + public private(set) var status: SocketIOStatus = .notConnected { + didSet { + switch status { + case .connected: + reconnecting = false + currentReconnectAttempt = 0 + default: + break + } + } + } + + /// 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.** + public var waitingPackets = [SocketPacket]() + + private(set) var reconnectAttempts = -1 + + private var _config: SocketIOClientConfiguration + private var currentReconnectAttempt = 0 + private var reconnecting = false + + /// Type safe way to create a new SocketIOClient. `opts` can be omitted. + /// + /// - parameter socketURL: The url of the socket.io server. + /// - parameter config: The config for this socket. + public init(socketURL: URL, config: SocketIOClientConfiguration = []) { + self._config = config + self.socketURL = socketURL + + if socketURL.absoluteString.hasPrefix("https://") { + self._config.insert(.secure(true)) + } + + self._config.insert(.path("/socket.io/"), replacing: false) + + super.init() + + setConfigs(_config) + } + + /// Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity. + /// If using Swift it's recommended to use `init(socketURL: NSURL, options: Set)` + /// + /// - parameter socketURL: The url of the socket.io server. + /// - parameter config: The config for this socket. + @objc + public convenience init(socketURL: NSURL, config: NSDictionary?) { + self.init(socketURL: socketURL as URL, config: config?.toSocketConfiguration() ?? []) + } + + deinit { + DefaultSocketLogger.Logger.log("Manager is being released", type: SocketManager.logType) + + engine?.disconnect(reason: "Manager Deinit") + } + + // MARK: Methods + + private func addEngine() { + DefaultSocketLogger.Logger.log("Adding engine", type: SocketManager.logType) + + engine?.engineQueue.sync { + self.engine?.client = nil + } + + engine = SocketEngine(client: self, url: socketURL, config: config) + } + + /// Connects the underlying transport and the default namespace socket. + /// + /// Override if you wish to attach a custom `SocketEngineSpec`. + open func connect() { + guard !status.active else { + DefaultSocketLogger.Logger.log("Tried connecting an already active socket", type: SocketManager.logType) + + return + } + + if engine == nil || forceNew { + addEngine() + } + + status = .connecting + + engine?.connect() + } + + /// Connects a socket through this manager's engine. + /// + /// - parameter socket: The socket who we should connect through this manager. + open func connectSocket(_ socket: SocketIOClient) { + guard status == .connected else { + DefaultSocketLogger.Logger.log("Tried connecting socket when engine isn't open. Connecting", + type: SocketManager.logType) + + connect() + return + } + + engine?.send("0\(socket.nsp)", withData: []) + } + + /// Called when the manager has disconnected from socket.io. + /// + /// - parameter reason: The reason for the disconnection. + open func didDisconnect(reason: String) { + forAll {socket in + socket.didDisconnect(reason: reason) + } + } + + /// Disconnects the manager and all associated sockets. + open func disconnect() { + DefaultSocketLogger.Logger.log("Manager closing", type: SocketManager.logType) + + status = .disconnected + + engine?.disconnect(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 + /// releasing. + /// + /// - parameter socket: The socket to disconnect. + open func disconnectSocket(_ socket: SocketIOClient) { + // Make sure we remove socket from nsps + nsps.removeValue(forKey: socket.nsp) + + engine?.send("1\(socket.nsp)", withData: []) + socket.didDisconnect(reason: "Namespace leave") + } + + /// Disconnects the socket associated with `forNamespace`. + /// + /// This will remove the socket for the manager's control, and make the socket instance useless and ready for + /// releasing. + /// + /// - parameter forNamespace: The namespace to disconnect from. + open func disconnectSocket(forNamespace nsp: String) { + guard let socket = nsps.removeValue(forKey: nsp) else { + DefaultSocketLogger.Logger.log("Could not find socket for \(nsp) to disconnect", + type: SocketManager.logType) + + return + } + + disconnectSocket(socket) + } + + /// Sends a client event to all sockets in `nsps` + /// + /// - parameter clientEvent: The event to emit. + open func emitAll(clientEvent event: SocketClientEvent, data: [Any]) { + forAll {socket in + socket.handleClientEvent(event, data: data) + } + } + + /// Sends an event to the server on all namespaces in this manager. + /// + /// - parameter event: The event to send. + /// - parameter items: The data to send with this event. + open func emitAll(_ event: String, _ items: SocketData...) { + guard let emitData = try? items.map({ try $0.socketRepresentation() }) else { + DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)", + type: SocketManager.logType) + + return + } + + emitAll(event, withItems: emitData) + } + + /// Sends an event to the server on all namespaces in this manager. + /// + /// Same as `emitAll(_:_:)`, but meant for Objective-C. + /// + /// - parameter event: The event to send. + /// - parameter withItems: The data to send with this event. + open func emitAll(_ event: String, withItems items: [Any]) { + forAll {socket in + socket.emit(event, with: items) + } + } + + /// Called when the engine closes. + /// + /// - parameter reason: The reason that the engine closed. + open func engineDidClose(reason: String) { + handleQueue.async { + self._engineDidClose(reason: reason) + } + } + + private func _engineDidClose(reason: String) { + waitingPackets.removeAll() + + if status != .disconnected { + status = .notConnected + } + + if status == .disconnected || !reconnects { + didDisconnect(reason: reason) + } else if !reconnecting { + reconnecting = true + tryReconnect(reason: reason) + } + } + + /// Called when the engine errors. + /// + /// - parameter reason: The reason the engine errored. + open func engineDidError(reason: String) { + handleQueue.async { + self._engineDidError(reason: reason) + } + } + + private func _engineDidError(reason: String) { + DefaultSocketLogger.Logger.error("\(reason)", type: SocketManager.logType) + + emitAll(clientEvent: .error, data: [reason]) + } + + /// Called when the engine opens. + /// + /// - parameter reason: The reason the engine opened. + open func engineDidOpen(reason: String) { + handleQueue.async { + self._engineDidOpen(reason: reason) + } + } + + private func _engineDidOpen(reason: String) { + DefaultSocketLogger.Logger.log("Engine opened \(reason)", type: SocketManager.logType) + + status = .connected + nsps["/"]?.didConnect(toNamespace: "/") + + for (nsp, socket) in nsps where nsp != "/" && socket.status == .connecting { + connectSocket(socket) + } + } + + /// Called when the engine receives a pong message. + open func engineDidReceivePong() { + handleQueue.async { + self._engineDidReceivePong() + } + } + + private func _engineDidReceivePong() { + emitAll(clientEvent: .pong, data: []) + } + + /// Called when the sends a ping to the server. + open func engineDidSendPing() { + handleQueue.async { + self._engineDidSendPing() + } + } + + private func _engineDidSendPing() { + emitAll(clientEvent: .ping, data: []) + } + + private func forAll(do: (SocketIOClient) throws -> ()) rethrows { + for (_, socket) in nsps { + try `do`(socket) + } + } + + /// Called when the engine has a message that must be parsed. + /// + /// - parameter msg: The message that needs parsing. + open func parseEngineMessage(_ msg: String) { + handleQueue.async { + self._parseEngineMessage(msg) + } + } + + private func _parseEngineMessage(_ msg: String) { + guard let packet = parseSocketMessage(msg) else { return } + guard packet.type != .binaryAck && packet.type != .binaryEvent else { + waitingPackets.append(packet) + + return + } + + nsps[packet.nsp]?.handlePacket(packet) + } + + /// Called when the engine receives binary data. + /// + /// - parameter data: The data the engine received. + open func parseEngineBinaryData(_ data: Data) { + handleQueue.async { + self._parseEngineBinaryData(data) + } + } + + private func _parseEngineBinaryData(_ data: Data) { + guard let packet = parseBinaryData(data) else { return } + + nsps[packet.nsp]?.handlePacket(packet) + } + + /// Tries to reconnect to the server. + /// + /// This will cause a `disconnect` event to be emitted, as well as an `reconnectAttempt` event. + open func reconnect() { + guard !reconnecting else { return } + + engine?.disconnect(reason: "manual reconnect") + } + + private func tryReconnect(reason: String) { + guard reconnecting else { return } + + DefaultSocketLogger.Logger.log("Starting reconnect", type: SocketManager.logType) + + // Set status to connecting and emit reconnect for all sockets + forAll {socket in + guard socket.status == .connected else { return } + + socket.setReconnecting(reason: reason) + } + + _tryReconnect() + } + + private func _tryReconnect() { + guard reconnects && reconnecting && status != .disconnected else { return } + + if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts { + return didDisconnect(reason: "Reconnect Failed") + } + + DefaultSocketLogger.Logger.log("Trying to reconnect", type: SocketManager.logType) + emitAll(clientEvent: .reconnectAttempt, data: [(reconnectAttempts - currentReconnectAttempt)]) + + currentReconnectAttempt += 1 + connect() + + handleQueue.asyncAfter(deadline: DispatchTime.now() + Double(reconnectWait), execute: _tryReconnect) + } + + /// Sets manager specific configs. + /// + /// parameter config: The configs that should be set. + open func setConfigs(_ config: SocketIOClientConfiguration) { + for option in config { + switch option { + case let .forceNew(new): + self.forceNew = new + case let .reconnects(reconnects): + self.reconnects = reconnects + case let .reconnectWait(wait): + reconnectWait = abs(wait) + case let .log(log): + DefaultSocketLogger.Logger.log = log + case let .logger(logger): + DefaultSocketLogger.Logger = logger + default: + continue + } + } + + _config = config + _config.insert(.path("/socket.io/"), replacing: false) + + // If `ConfigSettable` & `SocketEngineSpec`, update its configs. + if var settableEngine = engine as? ConfigSettable & SocketEngineSpec { + settableEngine.engineQueue.sync { + settableEngine.setConfigs(self._config) + } + + engine = settableEngine + } + } + + /// Returns a `SocketIOClient` for the given namespace. This socket shares a transport with the manager. + /// + /// Calling multiple times returns the same socket. + /// + /// Sockets created from this method are retained by the manager. + /// Call one of the `disconnectSocket` methods on this class to remove the socket from manager control. + /// Or call `SocketIOClient.disconnect()` on the client. + /// + /// - parameter forNamespace: The namespace for the socket. + /// - returns: A `SocketIOClient` for the given namespace. + open func socket(forNamespace nsp: String) -> SocketIOClient { + assert(nsp.hasPrefix("/"), "forNamespace must have a leading /") + + if let socket = nsps[nsp] { + return socket + } + + let client = SocketIOClient(manager: self, nsp: nsp) + + nsps[nsp] = client + + return client + } + + // Test properties + + func setTestStatus(_ status: SocketIOStatus) { + self.status = status + } +} diff --git a/Source/SocketIO/Manager/SocketManagerSpec.swift b/Source/SocketIO/Manager/SocketManagerSpec.swift new file mode 100644 index 0000000..9eb373c --- /dev/null +++ b/Source/SocketIO/Manager/SocketManagerSpec.swift @@ -0,0 +1,128 @@ +// +// Created by Erik Little on 10/18/17. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Dispatch +import Foundation + +// TODO Fix the types so that we aren't using concrete types + +/// +/// A manager for a socket.io connection. +/// +/// A `SocketManagerSpec` is responsible for multiplexing multiple namespaces through a single `SocketEngineSpec`. +/// +/// Example with `SocketManager`: +/// +/// ```swift +/// let manager = SocketManager(socketURL: URL(string:"http://localhost:8080/")!) +/// let defaultNamespaceSocket = manager.defaultSocket +/// let swiftSocket = manager.socket(forNamespace: "/swift") +/// +/// // defaultNamespaceSocket and swiftSocket both share a single connection to the server +/// ``` +/// +/// Sockets created through the manager are retained by the manager. So at the very least, a single strong reference +/// to the manager must be maintained to keep sockets alive. +/// +/// To disconnect a socket and remove it from the manager, either call `SocketIOClient.disconnect()` on the socket, +/// or call one of the `disconnectSocket` methods on this class. +/// +@objc +public protocol SocketManagerSpec : class, SocketEngineClient { + // MARK: Properties + + /// Returns the socket associated with the default namespace ("/"). + var defaultSocket: SocketIOClient? { get } + + /// The engine for this manager. + var engine: SocketEngineSpec? { get set } + + /// If `true` then every time `connect` is called, a new engine will be created. + var forceNew: Bool { get set } + + // TODO Per socket queues? + /// The queue that all interaction with the client should occur on. This is the queue that event handlers are + /// called on. + var handleQueue: DispatchQueue { get set } + + /// If `true`, this manager will try and reconnect on any disconnects. + var reconnects: Bool { get set } + + /// The number of seconds to wait before attempting to reconnect. + var reconnectWait: Int { get set } + + /// The URL of the socket.io server. + var socketURL: URL { get } + + /// The status of this manager. + var status: SocketIOStatus { get } + + // MARK: Methods + + /// Connects the underlying transport. + func connect() + + /// Connects a socket through this manager's engine. + /// + /// - parameter socket: The socket who we should connect through this manager. + func connectSocket(_ socket: SocketIOClient) + + /// Called when the manager has disconnected from socket.io. + /// + /// - parameter reason: The reason for the disconnection. + func didDisconnect(reason: String) + + /// Disconnects the manager and all associated sockets. + func disconnect() + + /// Disconnects the given socket. + /// + /// - parameter socket: The socket to disconnect. + func disconnectSocket(_ socket: SocketIOClient) + + /// Disconnects the socket associated with `forNamespace`. + /// + /// - parameter forNamespace: The namespace to disconnect from. + func disconnectSocket(forNamespace nsp: String) + + /// Sends an event to the server on all namespaces in this manager. + /// + /// - parameter event: The event to send. + /// - parameter withItems: The data to send with this event. + func emitAll(_ event: String, withItems items: [Any]) + + /// Tries to reconnect to the server. + /// + /// This will cause a `disconnect` event to be emitted, as well as an `reconnectAttempt` event. + func reconnect() + + /// Returns a `SocketIOClient` for the given namespace. This socket shares a transport with the manager. + /// + /// Calling multiple times returns the same socket. + /// + /// Sockets created from this method are retained by the manager. + /// Call one of the `disconnectSocket` methods on the implementing class to remove the socket from manager control. + /// Or call `SocketIOClient.disconnect()` on the client. + /// + /// - parameter forNamespace: The namespace for the socket. + /// - returns: A `SocketIOClient` for the given namespace. + func socket(forNamespace nsp: String) -> SocketIOClient +} diff --git a/Source/SocketIO/Parse/SocketPacket.swift b/Source/SocketIO/Parse/SocketPacket.swift index 780f524..28fbf35 100644 --- a/Source/SocketIO/Parse/SocketPacket.swift +++ b/Source/SocketIO/Parse/SocketPacket.swift @@ -142,12 +142,8 @@ public struct SocketPacket : CustomStringConvertible { if dict["_placeholder"] as? Bool ?? false { return binary[dict["num"] as! Int] } else { - return dict.reduce(JSON(), {cur, keyValue in - var cur = cur - + return dict.reduce(into: JSON(), {cur, keyValue in cur[keyValue.0] = _fillInPlaceholders(keyValue.1) - - return cur }) } case let arr as [Any]: @@ -225,12 +221,8 @@ private extension SocketPacket { case let arr as [Any]: return arr.map({shred($0, binary: &binary)}) case let dict as JSON: - return dict.reduce(JSON(), {cur, keyValue in - var mutCur = cur - - mutCur[keyValue.0] = shred(keyValue.1, binary: &binary) - - return mutCur + return dict.reduce(into: JSON(), {cur, keyValue in + cur[keyValue.0] = shred(keyValue.1, binary: &binary) }) default: return data diff --git a/Source/SocketIO/Parse/SocketParsable.swift b/Source/SocketIO/Parse/SocketParsable.swift index de1c87a..2349ce3 100644 --- a/Source/SocketIO/Parse/SocketParsable.swift +++ b/Source/SocketIO/Parse/SocketParsable.swift @@ -26,14 +26,6 @@ import Foundation 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. @@ -43,12 +35,13 @@ public protocol SocketParsable : class { /// into the correct placeholder. /// /// - parameter data: The data that should be attached to a packet. - func parseBinaryData(_ data: Data) + func parseBinaryData(_ data: Data) -> SocketPacket? /// 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) + /// - returns: A completed socket packet if there is no more data left to collect. + func parseSocketMessage(_ message: String) -> SocketPacket? } /// Errors that can be thrown during parsing. @@ -65,38 +58,18 @@ public enum SocketParsableError : Error { case invalidPacketType } -public extension SocketParsable where Self: SocketIOClientSpec { - private func isCorrectNamespace(_ nsp: String) -> Bool { - return nsp == self.nsp - } - - private func handleConnect(_ packetNamespace: String) { - guard packetNamespace == nsp else { return } - - didConnect(toNamespace: packetNamespace) - } - - private func handlePacket(_ pack: SocketPacket) { - switch pack.type { - case .event where isCorrectNamespace(pack.nsp): - handleEvent(pack.event, data: pack.args, isInternalMessage: false, withAck: pack.id) - case .ack where isCorrectNamespace(pack.nsp): - handleAck(pack.id, data: pack.data) - case .binaryEvent where isCorrectNamespace(pack.nsp): - waitingPackets.append(pack) - case .binaryAck where isCorrectNamespace(pack.nsp): - waitingPackets.append(pack) - case .connect: - handleConnect(pack.nsp) - case .disconnect: - didDisconnect(reason: "Got Disconnect") - case .error: - handleEvent("error", data: pack.data, isInternalMessage: true, withAck: pack.id) - default: - DefaultSocketLogger.Logger.log("Got invalid packet: \(pack.description)", type: "SocketParser") - } - } +/// Says that a type will be able to buffer binary data before all data for an event has come in. +public protocol SocketDataBufferable : class { + /// 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 } +} +public extension SocketParsable where Self: SocketManagerSpec & SocketDataBufferable { /// Parses a message from the engine, returning a complete SocketPacket or throwing. /// /// - parameter message: The message to parse. @@ -169,8 +142,9 @@ public 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. - public func parseSocketMessage(_ message: String) { - guard !message.isEmpty else { return } + /// - returns: A completed socket packet or nil if the packet is invalid. + public func parseSocketMessage(_ message: String) -> SocketPacket? { + guard !message.isEmpty else { return nil } DefaultSocketLogger.Logger.log("Parsing \(message)", type: "SocketParser") @@ -179,9 +153,11 @@ public extension SocketParsable where Self: SocketIOClientSpec { DefaultSocketLogger.Logger.log("Decoded packet as: \(packet.description)", type: "SocketParser") - handlePacket(packet) + return packet } catch { DefaultSocketLogger.Logger.error("\(error): \(message)", type: "SocketParser") + + return nil } } @@ -192,21 +168,17 @@ public extension SocketParsable where Self: SocketIOClientSpec { /// into the correct placeholder. /// /// - parameter data: The data that should be attached to a packet. - public func parseBinaryData(_ data: Data) { + /// - returns: A completed socket packet if there is no more data left to collect. + public func parseBinaryData(_ data: Data) -> SocketPacket? { guard !waitingPackets.isEmpty else { DefaultSocketLogger.Logger.error("Got data when not remaking packet", type: "SocketParser") - return + + return nil } // Should execute event? - guard waitingPackets[waitingPackets.count - 1].addData(data) else { return } + guard waitingPackets[waitingPackets.count - 1].addData(data) else { return nil } - let packet = waitingPackets.removeLast() - - if packet.type != .binaryAck { - handleEvent(packet.event, data: packet.args, isInternalMessage: false, withAck: packet.id) - } else { - handleAck(packet.id, data: packet.args) - } + return waitingPackets.removeLast() } } diff --git a/Source/SocketIO/Util/SocketClientManager.swift b/Source/SocketIO/Util/SocketClientManager.swift deleted file mode 100644 index 0c99309..0000000 --- a/Source/SocketIO/Util/SocketClientManager.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// SocketClientManager.swift -// Socket.IO-Client-Swift -// -// Created by Erik Little on 6/11/16. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/** - Experimental socket manager. - - API subject to change. - - Can be used to persist sockets across ViewControllers. - - Sockets are strongly stored, so be sure to remove them once they are no - longer needed. - - Example usage: - ``` - let manager = SocketClientManager.sharedManager - manager["room1"] = socket1 - manager["room2"] = socket2 - manager.removeSocket(socket: socket2) - manager["room1"]?.emit("hello") - ``` - */ -open class SocketClientManager : NSObject { - // MARK: Properties. - - /// The shared manager. - @objc - open static let sharedManager = SocketClientManager() - - private var sockets = [String: SocketIOClient]() - - /// Gets a socket by its name. - /// - /// - returns: The socket, if one had the given name. - open subscript(string: String) -> SocketIOClient? { - get { - return sockets[string] - } - - set(socket) { - sockets[string] = socket - } - } - - // MARK: Methods. - - /// Adds a socket. - /// - /// - parameter socket: The socket to add. - /// - parameter labeledAs: The label for this socket. - @objc - open func addSocket(_ socket: SocketIOClient, labeledAs label: String) { - sockets[label] = socket - } - - /// Removes a socket by a given name. - /// - /// - parameter withLabel: The label of the socket to remove. - /// - returns: The socket for the given label, if one was present. - @objc - @discardableResult - open func removeSocket(withLabel label: String) -> SocketIOClient? { - return sockets.removeValue(forKey: label) - } - - /// Removes a socket. - /// - /// - parameter socket: The socket to remove. - /// - returns: The socket if it was in the manager. - @objc - @discardableResult - open func removeSocket(_ socket: SocketIOClient) -> SocketIOClient? { - var returnSocket: SocketIOClient? - - for (label, dictSocket) in sockets where dictSocket === socket { - returnSocket = sockets.removeValue(forKey: label) - } - - return returnSocket - } - - /// Removes all the sockets in the manager. - @objc - open func removeSockets() { - sockets.removeAll() - } -} diff --git a/Source/SocketIO/Util/SocketExtensions.swift b/Source/SocketIO/Util/SocketExtensions.swift index e80cf6e..d05555e 100644 --- a/Source/SocketIO/Util/SocketExtensions.swift +++ b/Source/SocketIO/Util/SocketExtensions.swift @@ -63,8 +63,6 @@ extension NSDictionary { return .log(log) case let ("logger", logger as SocketLogger): return .logger(logger) - case let ("nsp", nsp as String): - return .nsp(nsp) case let ("path", path as String): return .path(path) case let ("reconnects", reconnects as Bool): diff --git a/Tests/TestSocketIO/SocketBasicPacketTest.swift b/Tests/TestSocketIO/SocketBasicPacketTest.swift index fb947a0..b1c36ad 100644 --- a/Tests/TestSocketIO/SocketBasicPacketTest.swift +++ b/Tests/TestSocketIO/SocketBasicPacketTest.swift @@ -150,10 +150,9 @@ class SocketBasicPacketTest: XCTestCase { func testBinaryStringPlaceholderInMessage() { let engineString = "52-[\"test\",\"~~0\",{\"num\":0,\"_placeholder\":true},{\"_placeholder\":true,\"num\":1}]" - let socket = SocketIOClient(socketURL: URL(string: "http://localhost/")!) - socket.setTestable() + let manager = SocketManager(socketURL: URL(string: "http://localhost/")!) - var packet = try! socket.parseString(engineString) + var packet = try! manager.parseString(engineString) XCTAssertEqual(packet.event, "test") _ = packet.addData(data) diff --git a/Tests/TestSocketIO/SocketEngineTest.swift b/Tests/TestSocketIO/SocketEngineTest.swift index c8cc46d..048100d 100644 --- a/Tests/TestSocketIO/SocketEngineTest.swift +++ b/Tests/TestSocketIO/SocketEngineTest.swift @@ -10,20 +10,9 @@ import XCTest @testable import SocketIO class SocketEngineTest: XCTestCase { - var client: SocketIOClient! - var engine: SocketEngine! - - override func setUp() { - super.setUp() - client = SocketIOClient(socketURL: URL(string: "http://localhost")!) - engine = SocketEngine(client: client, url: URL(string: "http://localhost")!, options: nil) - - client.setTestable() - } - func testBasicPollingMessage() { let expect = expectation(description: "Basic polling test") - client.on("blankTest") {data, ack in + socket.on("blankTest") {data, ack in expect.fulfill() } @@ -35,11 +24,11 @@ class SocketEngineTest: XCTestCase { let finalExpectation = expectation(description: "Final packet in poll test") var gotBlank = false - client.on("blankTest") {data, ack in + socket.on("blankTest") {data, ack in gotBlank = true } - client.on("stringTest") {data, ack in + socket.on("stringTest") {data, ack in if let str = data[0] as? String, gotBlank { if str == "hello" { finalExpectation.fulfill() @@ -54,7 +43,7 @@ class SocketEngineTest: XCTestCase { func testEngineDoesErrorOnUnknownTransport() { let finalExpectation = expectation(description: "Unknown Transport") - client.on("error") {data, ack in + socket.on("error") {data, ack in if let error = data[0] as? String, error == "Unknown transport" { finalExpectation.fulfill() } @@ -67,7 +56,7 @@ class SocketEngineTest: XCTestCase { func testEngineDoesErrorOnUnknownMessage() { let finalExpectation = expectation(description: "Engine Errors") - client.on("error") {data, ack in + socket.on("error") {data, ack in finalExpectation.fulfill() } @@ -78,7 +67,7 @@ class SocketEngineTest: XCTestCase { func testEngineDecodesUTF8Properly() { let expect = expectation(description: "Engine Decodes utf8") - client.on("stringTest") {data, ack in + socket.on("stringTest") {data, ack in XCTAssertEqual(data[0] as? String, "lïne one\nlīne \rtwo𦅙𦅛", "Failed string test") expect.fulfill() } @@ -110,7 +99,7 @@ class SocketEngineTest: XCTestCase { let b64String = "b4aGVsbG8NCg==" let packetString = "451-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]" - client.on("test") {data, ack in + socket.on("test") {data, ack in if let data = data[0] as? Data, let string = String(data: data, encoding: .utf8) { XCTAssertEqual(string, "hello") } @@ -123,4 +112,97 @@ class SocketEngineTest: XCTestCase { waitForExpectations(timeout: 3, handler: nil) } + + func testSettingExtraHeadersBeforeConnectSetsEngineExtraHeaders() { + let newValue = ["hello": "world"] + + manager.engine = engine + manager.setTestStatus(.notConnected) + manager.config = [.extraHeaders(["new": "value"])] + manager.config.insert(.extraHeaders(newValue), replacing: true) + + XCTAssertEqual(2, manager.config.count) + XCTAssertEqual(manager.engine!.extraHeaders!, newValue) + + for config in manager.config { + switch config { + case let .extraHeaders(headers): + XCTAssertTrue(headers.keys.contains("hello"), "It should contain hello header key") + XCTAssertFalse(headers.keys.contains("new"), "It should not contain old data") + case .path: + continue + default: + XCTFail("It should only have two configs") + } + } + } + + func testSettingExtraHeadersAfterConnectDoesNotIgnoreChanges() { + let newValue = ["hello": "world"] + + manager.engine = engine + manager.setTestStatus(.connected) + engine.setConnected(true) + manager.config = [.extraHeaders(["new": "value"])] + manager.config.insert(.extraHeaders(["hello": "world"]), replacing: true) + + XCTAssertEqual(2, manager.config.count) + XCTAssertEqual(manager.engine!.extraHeaders!, newValue) + } + + func testSettingPathAfterConnectDoesNotIgnoreChanges() { + let newValue = "/newpath/" + + manager.engine = engine + manager.setTestStatus(.connected) + engine.setConnected(true) + manager.config.insert(.path(newValue)) + + XCTAssertEqual(1, manager.config.count) + XCTAssertEqual(manager.engine!.socketPath, newValue) + } + + func testSettingCompressAfterConnectDoesNotIgnoreChanges() { + manager.engine = engine + manager.setTestStatus(.connected) + engine.setConnected(true) + manager.config.insert(.compress) + + XCTAssertEqual(2, manager.config.count) + XCTAssertTrue(manager.engine!.compress) + } + + func testSettingForcePollingAfterConnectDoesNotIgnoreChanges() { + manager.engine = engine + manager.setTestStatus(.connected) + engine.setConnected(true) + manager.config.insert(.forcePolling(true)) + + XCTAssertEqual(2, manager.config.count) + XCTAssertTrue(manager.engine!.forcePolling) + } + + func testSettingForceWebSocketsAfterConnectDoesNotIgnoreChanges() { + manager.engine = engine + manager.setTestStatus(.connected) + engine.setConnected(true) + manager.config.insert(.forceWebsockets(true)) + + XCTAssertEqual(2, manager.config.count) + XCTAssertTrue(manager.engine!.forceWebsockets) + } + + var manager: SocketManager! + var socket: SocketIOClient! + var engine: SocketEngine! + + override func setUp() { + super.setUp() + + manager = SocketManager(socketURL: URL(string: "http://localhost")!) + socket = manager.defaultSocket + engine = SocketEngine(client: manager, url: URL(string: "http://localhost")!, options: nil) + + socket.setTestable() + } } diff --git a/Tests/TestSocketIO/SocketIOClientConfigurationTest.swift b/Tests/TestSocketIO/SocketIOClientConfigurationTest.swift index 9b5c50a..1289a11 100644 --- a/Tests/TestSocketIO/SocketIOClientConfigurationTest.swift +++ b/Tests/TestSocketIO/SocketIOClientConfigurationTest.swift @@ -9,20 +9,12 @@ import XCTest import SocketIO -class TestSocketIOClientConfiguration: XCTestCase { - var config = [] as SocketIOClientConfiguration - - override func setUp() { - super.setUp() - - config = [.log(false), .forceNew(true)] - } - +class TestSocketIOClientConfiguration : XCTestCase { func testReplaceSameOption() { config.insert(.log(true)) - + XCTAssertEqual(config.count, 2) - + switch config[0] { case let .log(log): XCTAssertTrue(log) @@ -30,12 +22,12 @@ class TestSocketIOClientConfiguration: XCTestCase { XCTFail() } } - + func testIgnoreIfExisting() { config.insert(.forceNew(false), replacing: false) - + XCTAssertEqual(config.count, 2) - + switch config[1] { case let .forceNew(new): XCTAssertTrue(new) @@ -43,4 +35,12 @@ class TestSocketIOClientConfiguration: XCTestCase { XCTFail() } } + + var config = [] as SocketIOClientConfiguration + + override func setUp() { + config = [.log(false), .forceNew(true)] + + super.setUp() + } } diff --git a/Tests/TestSocketIO/SocketMangerTest.swift b/Tests/TestSocketIO/SocketMangerTest.swift new file mode 100644 index 0000000..25e6d2b --- /dev/null +++ b/Tests/TestSocketIO/SocketMangerTest.swift @@ -0,0 +1,174 @@ +// +// Created by Erik Little on 10/21/17. +// + +import Dispatch +import Foundation +@testable import SocketIO +import XCTest + +class SocketMangerTest : XCTestCase { + func testManagerProperties() { + XCTAssertNotNil(manager.defaultSocket) + XCTAssertNil(manager.engine) + XCTAssertFalse(manager.forceNew) + XCTAssertEqual(manager.handleQueue, DispatchQueue.main) + XCTAssertTrue(manager.reconnects) + XCTAssertEqual(manager.reconnectWait, 10) + XCTAssertEqual(manager.status, .notConnected) + } + + func testManagerCallsConnect() { + setUpSockets() + + socket.expectations[ManagerExpectation.didConnectCalled] = expectation(description: "The manager should call connect on the default socket") + socket2.expectations[ManagerExpectation.didConnectCalled] = expectation(description: "The manager should call connect on the socket") + + socket.connect() + socket2.connect() + + manager.fakeConnecting() + manager.fakeConnecting(toNamespace: "/swift") + + waitForExpectations(timeout: 0.3) + } + + func testManagerCallsDisconnect() { + setUpSockets() + + socket.expectations[ManagerExpectation.didDisconnectCalled] = expectation(description: "The manager should call disconnect on the default socket") + socket2.expectations[ManagerExpectation.didDisconnectCalled] = expectation(description: "The manager should call disconnect on the socket") + + socket2.on(clientEvent: .connect) {data, ack in + self.manager.disconnect() + self.manager.fakeDisconnecting() + } + + socket.connect() + socket2.connect() + + manager.fakeConnecting() + manager.fakeConnecting(toNamespace: "/swift") + + waitForExpectations(timeout: 0.3) + } + + func testManagerEmitAll() { + setUpSockets() + + socket.expectations[ManagerExpectation.emitAllEventCalled] = expectation(description: "The manager should emit an event to the default socket") + socket2.expectations[ManagerExpectation.emitAllEventCalled] = expectation(description: "The manager should emit an event to the socket") + + socket2.on(clientEvent: .connect) {data, ack in + self.manager.emitAll("event", "testing") + } + + socket.connect() + socket2.connect() + + manager.fakeConnecting() + manager.fakeConnecting(toNamespace: "/swift") + + waitForExpectations(timeout: 0.3) + } + + private func setUpSockets() { + socket = manager.testSocket(forNamespace: "/") + socket2 = manager.testSocket(forNamespace: "/swift") + } + + private var manager: TestManager! + private var socket: TestSocket! + private var socket2: TestSocket! + + override func setUp() { + super.setUp() + + manager = TestManager(socketURL: URL(string: "http://localhost/")!) + socket = nil + socket2 = nil + } +} + +public enum ManagerExpectation : String { + case didConnectCalled + case didDisconnectCalled + case emitAllEventCalled +} + +public class TestManager : SocketManager { + public override func disconnect() { + setTestStatus(.disconnected) + } + + @objc + public func testSocket(forNamespace nsp: String) -> TestSocket { + return socket(forNamespace: nsp) as! TestSocket + } + + @objc + public func fakeConnecting(toNamespace nsp: String) { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { + // Fake connecting + self.parseEngineMessage("0\(nsp)") + } + } + + @objc + public func fakeDisconnecting() { + engineDidClose(reason: "") + } + + @objc + public func fakeConnecting() { + engineDidOpen(reason: "") + } + + public override func socket(forNamespace nsp: String) -> SocketIOClient { + // set socket to our test socket, the superclass method will get this from nsps + nsps[nsp] = TestSocket(manager: self, nsp: nsp) + + return super.socket(forNamespace: nsp) + } +} + +public class TestSocket : SocketIOClient { + public var expectations = [ManagerExpectation: XCTestExpectation]() + + @objc + public var expects = NSMutableDictionary() + + public override func didConnect(toNamespace nsp: String) { + expectations[ManagerExpectation.didConnectCalled]?.fulfill() + expectations[ManagerExpectation.didConnectCalled] = nil + + if let expect = expects[ManagerExpectation.didConnectCalled.rawValue] as? XCTestExpectation { + expect.fulfill() + expects[ManagerExpectation.didConnectCalled.rawValue] = nil + } + + super.didConnect(toNamespace: nsp) + } + + public override func didDisconnect(reason: String) { + expectations[ManagerExpectation.didDisconnectCalled]?.fulfill() + expectations[ManagerExpectation.didDisconnectCalled] = nil + + if let expect = expects[ManagerExpectation.didDisconnectCalled.rawValue] as? XCTestExpectation { + expect.fulfill() + expects[ManagerExpectation.didDisconnectCalled.rawValue] = nil + } + + super.didDisconnect(reason: reason) + } + + public override func emit(_ event: String, with items: [Any]) { + expectations[ManagerExpectation.emitAllEventCalled]?.fulfill() + expectations[ManagerExpectation.emitAllEventCalled] = nil + + if let expect = expects[ManagerExpectation.emitAllEventCalled.rawValue] as? XCTestExpectation { + expect.fulfill() + expects[ManagerExpectation.emitAllEventCalled.rawValue] = nil + } + } +} diff --git a/Tests/TestSocketIO/SocketParserTest.swift b/Tests/TestSocketIO/SocketParserTest.swift index e22de7c..285d7e8 100644 --- a/Tests/TestSocketIO/SocketParserTest.swift +++ b/Tests/TestSocketIO/SocketParserTest.swift @@ -10,26 +10,6 @@ import XCTest @testable import SocketIO class SocketParserTest: XCTestCase { - let testSocket = SocketIOClient(socketURL: URL(string: "http://localhost/")!) - - //Format key: message; namespace-data-binary-id - static let packetTypes: [String: (String, [Any], [Data], Int)] = [ - "0": ("/", [], [], -1), "1": ("/", [], [], -1), - "25[\"test\"]": ("/", ["test"], [], 5), - "2[\"test\",\"~~0\"]": ("/", ["test", "~~0"], [], -1), - "2/swift,[\"testArrayEmitReturn\",[\"test3\",\"test4\"]]": ("/swift", ["testArrayEmitReturn", ["test3", "test4"] as NSArray], [], -1), - "51-/swift,[\"testMultipleItemsWithBufferEmitReturn\",[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]": ("/swift", ["testMultipleItemsWithBufferEmitReturn", [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], -1), - "3/swift,0[[\"test3\",\"test4\"]]": ("/swift", [["test3", "test4"] as NSArray], [], 0), - "61-/swift,19[[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]": - ("/swift", [ [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], 19), - "4/swift,": ("/swift", [], [], -1), - "0/swift": ("/swift", [], [], -1), - "1/swift": ("/swift", [], [], -1), - "4\"ERROR\"": ("/", ["ERROR"], [], -1), - "4{\"test\":2}": ("/", [["test": 2]], [], -1), - "41": ("/", [1], [], -1), - "4[1, \"hello\"]": ("/", [1, "hello"], [], -1)] - func testDisconnect() { let message = "1" validateParseResult(message) @@ -108,7 +88,7 @@ class SocketParserTest: XCTestCase { func testInvalidInput() { let message = "8" do { - let _ = try testSocket.parseString(message) + let _ = try testManager.parseString(message) XCTFail() } catch { @@ -125,7 +105,7 @@ class SocketParserTest: XCTestCase { func validateParseResult(_ message: String) { let validValues = SocketParserTest.packetTypes[message]! - let packet = try! testSocket.parseString(message) + let packet = try! testManager.parseString(message) let type = String(message.characters.prefix(1)) XCTAssertEqual(packet.type, SocketPacket.PacketType(rawValue: Int(type) ?? -1)!) @@ -139,8 +119,29 @@ class SocketParserTest: XCTestCase { let keys = Array(SocketParserTest.packetTypes.keys) measure { for item in keys.enumerated() { - _ = try! self.testSocket.parseString(item.element) + _ = try! self.testManager.parseString(item.element) } } } + + let testManager = SocketManager(socketURL: URL(string: "http://localhost/")!) + + //Format key: message; namespace-data-binary-id + static let packetTypes: [String: (String, [Any], [Data], Int)] = [ + "0": ("/", [], [], -1), "1": ("/", [], [], -1), + "25[\"test\"]": ("/", ["test"], [], 5), + "2[\"test\",\"~~0\"]": ("/", ["test", "~~0"], [], -1), + "2/swift,[\"testArrayEmitReturn\",[\"test3\",\"test4\"]]": ("/swift", ["testArrayEmitReturn", ["test3", "test4"] as NSArray], [], -1), + "51-/swift,[\"testMultipleItemsWithBufferEmitReturn\",[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]": ("/swift", ["testMultipleItemsWithBufferEmitReturn", [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], -1), + "3/swift,0[[\"test3\",\"test4\"]]": ("/swift", [["test3", "test4"] as NSArray], [], 0), + "61-/swift,19[[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]": + ("/swift", [ [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], 19), + "4/swift,": ("/swift", [], [], -1), + "0/swift": ("/swift", [], [], -1), + "1/swift": ("/swift", [], [], -1), + "4\"ERROR\"": ("/", ["ERROR"], [], -1), + "4{\"test\":2}": ("/", [["test": 2]], [], -1), + "41": ("/", [1], [], -1), + "4[1, \"hello\"]": ("/", [1, "hello"], [], -1) + ] } diff --git a/Tests/TestSocketIO/SocketSideEffectTest.swift b/Tests/TestSocketIO/SocketSideEffectTest.swift index c487dd1..75a9a56 100644 --- a/Tests/TestSocketIO/SocketSideEffectTest.swift +++ b/Tests/TestSocketIO/SocketSideEffectTest.swift @@ -34,7 +34,7 @@ class SocketSideEffectTest: XCTestCase { expect.fulfill() } - socket.parseSocketMessage("30[\"hello world\"]") + manager.parseEngineMessage("30[\"hello world\"]") waitForExpectations(timeout: 3, handler: nil) } @@ -45,8 +45,8 @@ class SocketSideEffectTest: XCTestCase { expect.fulfill() } - socket.parseSocketMessage("61-0[{\"_placeholder\":true,\"num\":0},{\"test\":true}]") - socket.parseBinaryData(Data()) + manager.parseEngineMessage("61-0[{\"_placeholder\":true,\"num\":0},{\"test\":true}]") + manager.parseEngineBinaryData(Data()) waitForExpectations(timeout: 3, handler: nil) } @@ -57,7 +57,7 @@ class SocketSideEffectTest: XCTestCase { expect.fulfill() } - socket.parseSocketMessage("2[\"test\",\"hello world\"]") + manager.parseEngineMessage("2[\"test\",\"hello world\"]") waitForExpectations(timeout: 3, handler: nil) } @@ -68,7 +68,7 @@ class SocketSideEffectTest: XCTestCase { expect.fulfill() } - socket.parseSocketMessage("2[\"test\",\"\\\"hello world\\\"\"]") + manager.parseEngineMessage("2[\"test\",\"\\\"hello world\\\"\"]") waitForExpectations(timeout: 3, handler: nil) } @@ -80,7 +80,7 @@ class SocketSideEffectTest: XCTestCase { expect.fulfill() } - socket.parseSocketMessage("2[\"test\",\"hello world\"]") + manager.parseEngineMessage("2[\"test\",\"hello world\"]") waitForExpectations(timeout: 3, handler: nil) } @@ -96,7 +96,7 @@ class SocketSideEffectTest: XCTestCase { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { // Fake connecting - self.socket.parseEngineMessage("0/") + self.manager.parseEngineMessage("0/") } waitForExpectations(timeout: 3, handler: nil) @@ -136,59 +136,47 @@ class SocketSideEffectTest: XCTestCase { } } - socket.parseSocketMessage("4\"test error\"") + manager.parseEngineMessage("4\"test error\"") waitForExpectations(timeout: 3, handler: nil) } func testHandleBinaryEvent() { let expect = expectation(description: "handled binary event") socket.on("test") {data, ack in - if let dict = data[0] as? NSDictionary, let data = dict["test"] as? NSData { + if let dict = data[0] as? [String: Any], let data = dict["test"] as? Data { XCTAssertEqual(data as Data, self.data) expect.fulfill() } } - socket.parseSocketMessage("51-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]") - socket.parseBinaryData(data) + manager.parseEngineMessage("51-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]") + manager.parseEngineBinaryData(data) waitForExpectations(timeout: 3, handler: nil) } func testHandleMultipleBinaryEvent() { let expect = expectation(description: "handled multiple binary event") socket.on("test") {data, ack in - if let dict = data[0] as? NSDictionary, let data = dict["test"] as? NSData, - let data2 = dict["test2"] as? NSData { - XCTAssertEqual(data as Data, self.data) - XCTAssertEqual(data2 as Data, self.data2) - expect.fulfill() + if let dict = data[0] as? [String: Any], let data = dict["test"] as? Data, + let data2 = dict["test2"] as? Data { + XCTAssertEqual(data as Data, self.data) + XCTAssertEqual(data2 as Data, self.data2) + expect.fulfill() } } - socket.parseSocketMessage("52-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0},\"test2\":{\"_placeholder\":true,\"num\":1}}]") - socket.parseBinaryData(data) - socket.parseBinaryData(data2) + manager.parseEngineMessage("52-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0},\"test2\":{\"_placeholder\":true,\"num\":1}}]") + manager.parseEngineBinaryData(data) + manager.parseEngineBinaryData(data2) waitForExpectations(timeout: 3, handler: nil) } - func testSocketManager() { - let manager = SocketClientManager.sharedManager - manager["test"] = socket - - XCTAssert(manager["test"] === socket, "failed to get socket") - - manager["test"] = nil - - XCTAssert(manager["test"] == nil, "socket not removed") - - } - func testChangingStatusCallsStatusChangeHandler() { let expect = expectation(description: "The client should announce when the status changes") - let statusChange = SocketIOClientStatus.connecting + let statusChange = SocketIOStatus.connecting socket.on("statusChange") {data, ack in - guard let status = data[0] as? SocketIOClientStatus else { + guard let status = data[0] as? SocketIOStatus else { XCTFail("Status should be one of the defined statuses") return @@ -251,9 +239,9 @@ class SocketSideEffectTest: XCTestCase { func testConnectTimesOutIfNotConnected() { let expect = expectation(description: "The client should call the timeout function") + socket = manager.socket(forNamespace: "/someNamespace") socket.setTestStatus(.notConnected) - socket.nsp = "/someNamespace" - socket.engine = TestEngine(client: socket, url: socket.socketURL, options: nil) + manager.engine = TestEngine(client: manager, url: manager.socketURL, options: nil) socket.connect(timeoutAfter: 0.5, withHandler: { expect.fulfill() @@ -266,7 +254,7 @@ class SocketSideEffectTest: XCTestCase { let expect = expectation(description: "The client should not call the timeout function") socket.setTestStatus(.notConnected) - socket.engine = TestEngine(client: socket, url: socket.socketURL, options: nil) + manager.engine = TestEngine(client: manager, url: manager.socketURL, options: nil) socket.on(clientEvent: .connect) {data, ack in expect.fulfill() @@ -278,7 +266,7 @@ class SocketSideEffectTest: XCTestCase { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { // Fake connecting - self.socket.parseEngineMessage("0/") + self.manager.parseEngineMessage("0/") } waitForExpectations(timeout: 2) @@ -288,7 +276,7 @@ class SocketSideEffectTest: XCTestCase { let expect = expectation(description: "The client call the connect handler") socket.setTestStatus(.notConnected) - socket.engine = TestEngine(client: socket, url: socket.socketURL, options: nil) + manager.engine = TestEngine(client: manager, url: manager.socketURL, options: nil) socket.on(clientEvent: .connect) {data, ack in expect.fulfill() @@ -305,9 +293,9 @@ class SocketSideEffectTest: XCTestCase { let expect = expectation(description: "The client should not call the timeout function") let nspString = "/swift" + socket = manager.socket(forNamespace: "/swift") socket.setTestStatus(.notConnected) - socket.nsp = nspString - socket.engine = TestEngine(client: socket, url: socket.socketURL, options: nil) + manager.engine = TestEngine(client: manager, url: manager.socketURL, options: nil) socket.on(clientEvent: .connect) {data, ack in guard let nsp = data[0] as? String else { @@ -327,7 +315,7 @@ class SocketSideEffectTest: XCTestCase { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { // Fake connecting - self.socket.parseEngineMessage("0/swift") + self.manager.parseEngineMessage("0/swift") } waitForExpectations(timeout: 2) @@ -377,39 +365,31 @@ class SocketSideEffectTest: XCTestCase { func testSettingConfigAfterInit() { socket.setTestStatus(.notConnected) - socket.config.insert(.log(true)) + manager.config.insert(.log(true)) XCTAssertTrue(DefaultSocketLogger.Logger.log, "It should set logging to true after creation") - socket.config = [.log(false), .nsp("/test")] + manager.config = [.log(false)] XCTAssertFalse(DefaultSocketLogger.Logger.log, "It should set logging to false after creation") - XCTAssertEqual(socket.nsp, "/test", "It should set the namespace after creation") } - func testSettingExtraHeadersAfterInit() { - socket.setTestStatus(.notConnected) - socket.config = [.extraHeaders(["new": "value"])] - socket.config.insert(.extraHeaders(["hello": "world"]), replacing: true) + func testSettingConfigAfterDisconnect() { + socket.setTestStatus(.disconnected) + manager.config.insert(.log(true)) - for config in socket.config { - switch config { - case let .extraHeaders(headers): - XCTAssertTrue(headers.keys.contains("hello"), "It should contain hello header key") - XCTAssertFalse(headers.keys.contains("new"), "It should not contain old data") - case .path: - continue - default: - XCTFail("It should only have two configs") - } - } - } + XCTAssertTrue(DefaultSocketLogger.Logger.log, "It should set logging to true after creation") - func testSettingConfigAfterInitWhenConnectedIgnoresChanges() { - socket.config = [.log(true), .nsp("/test")] + manager.config = [.log(false)] XCTAssertFalse(DefaultSocketLogger.Logger.log, "It should set logging to false after creation") - XCTAssertEqual(socket.nsp, "/", "It should set the namespace after creation") + } + + func testSettingConfigAfterInitWhenConnectedDoesNotIgnoreChanges() { + manager.connect() + manager.config = [.log(true)] + + XCTAssertTrue(DefaultSocketLogger.Logger.log, "It should set logging to false after creation") } func testClientCallsSentPingHandler() { @@ -419,7 +399,7 @@ class SocketSideEffectTest: XCTestCase { expect.fulfill() } - socket.engineDidSendPing() + manager.engineDidSendPing() waitForExpectations(timeout: 0.2) } @@ -431,18 +411,22 @@ class SocketSideEffectTest: XCTestCase { expect.fulfill() } - socket.engineDidReceivePong() + manager.engineDidReceivePong() waitForExpectations(timeout: 0.2) } let data = "test".data(using: String.Encoding.utf8)! let data2 = "test2".data(using: String.Encoding.utf8)! + + private var manager: SocketManager! private var socket: SocketIOClient! override func setUp() { super.setUp() - socket = SocketIOClient(socketURL: URL(string: "http://localhost/")!) + + manager = SocketManager(socketURL: URL(string: "http://localhost/")!) + socket = manager.defaultSocket socket.setTestable() } } @@ -461,6 +445,7 @@ struct ThrowingData : SocketData { class TestEngine : SocketEngineSpec { weak var client: SocketEngineClient? private(set) var closed = false + private(set) var compress = false private(set) var connected = false var connectParams: [String: Any]? = nil private(set) var cookies: [HTTPCookie]? = nil diff --git a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.h b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.h new file mode 100644 index 0000000..e715321 --- /dev/null +++ b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.h @@ -0,0 +1,16 @@ +// +// Created by Erik Little on 10/21/17. +// + +#import "SocketIO_Tests-Swift.h" + +@import XCTest; +@import SocketIO; + +@interface ManagerObjectiveCTest : XCTestCase + +@property TestSocket* socket; +@property TestSocket* socket2; +@property TestManager* manager; + +@end diff --git a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m new file mode 100644 index 0000000..3994023 --- /dev/null +++ b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m @@ -0,0 +1,115 @@ +// +// Created by Erik Little on 10/21/17. +// + +#import "ManagerObjectiveCTest.h" + +@import Dispatch; +@import Foundation; +@import XCTest; +@import SocketIO; + +@implementation ManagerObjectiveCTest + +- (void)testManagerProperties { + XCTAssertNotNil(self.manager.defaultSocket); + XCTAssertNil(self.manager.engine); + XCTAssertFalse(self.manager.forceNew); + XCTAssertEqual(self.manager.handleQueue, dispatch_get_main_queue()); + XCTAssertTrue(self.manager.reconnects); + XCTAssertEqual(self.manager.reconnectWait, 10); + XCTAssertEqual(self.manager.status, SocketIOStatusNotConnected); +} + +- (void)testConnectSocketSyntax { + [self setUpSockets]; + [self.manager connectSocket:self.socket]; +} + +- (void)testDisconnectSocketSyntax { + [self setUpSockets]; + [self.manager disconnectSocket:self.socket]; +} + +- (void)testSocketForNamespaceSyntax { + SocketIOClient* client = [self.manager socketForNamespace:@"/swift"]; + client = nil; +} + +- (void)testManagerCallsConnect { + [self setUpSockets]; + + XCTestExpectation* expect = [self expectationWithDescription:@"The manager should call connect on the default socket"]; + XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should call connect on the socket"]; + + self.socket.expects[@"didConnectCalled"] = expect; + self.socket2.expects[@"didConnectCalled"] = expect2; + + [self.socket connect]; + [self.socket2 connect]; + + [self.manager fakeConnecting]; + [self.manager fakeConnectingToNamespace:@"/swift"]; + + [self waitForExpectationsWithTimeout:0.3 handler:nil]; +} + +- (void)testManagerCallsDisconnect { + [self setUpSockets]; + + XCTestExpectation* expect = [self expectationWithDescription:@"The manager should call disconnect on the default socket"]; + XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should call disconnect on the socket"]; + + self.socket.expects[@"didDisconnectCalled"] = expect; + self.socket2.expects[@"didDisconnectCalled"] = expect2; + + [self.socket2 on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) { + [self.manager disconnect]; + [self.manager fakeDisconnecting]; + }]; + + [self.socket connect]; + [self.socket2 connect]; + + [self.manager fakeConnecting]; + [self.manager fakeConnectingToNamespace:@"/swift"]; + + [self waitForExpectationsWithTimeout:0.3 handler:nil]; +} + +- (void)testManagerEmitAll { + [self setUpSockets]; + + XCTestExpectation* expect = [self expectationWithDescription:@"The manager should emit an event to the default socket"]; + XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should emit an event to the socket"]; + + self.socket.expects[@"emitAllEventCalled"] = expect; + self.socket2.expects[@"emitAllEventCalled"] = expect2; + + [self.socket2 on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) { + [self.manager emitAll:@"event" withItems:@[@"testing"]]; + }]; + + [self.socket connect]; + [self.socket2 connect]; + + [self.manager fakeConnecting]; + [self.manager fakeConnectingToNamespace:@"/swift"]; + + [self waitForExpectationsWithTimeout:0.3 handler:nil]; +} + +- (void)setUpSockets { + self.socket = [self.manager testSocketForNamespace:@"/"]; + self.socket2 = [self.manager testSocketForNamespace:@"/swift"]; +} + +- (void)setUp { + [super setUp]; + NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"]; + self.manager = [[TestManager alloc] initWithSocketURL:url config:nil]; + self.socket = nil; + self.socket2 = nil; +} + +@end diff --git a/Tests/TestSocketIOObjc/SocketObjectiveCTest.h b/Tests/TestSocketIOObjc/SocketObjectiveCTest.h new file mode 100644 index 0000000..1d0de3e --- /dev/null +++ b/Tests/TestSocketIOObjc/SocketObjectiveCTest.h @@ -0,0 +1,16 @@ +// +// Created by Erik Little on 10/21/17. +// + + +@import Dispatch; +@import Foundation; +@import XCTest; +@import SocketIO; + +@interface SocketObjectiveCTest : XCTestCase + +@property SocketIOClient* socket; +@property SocketManager* manager; + +@end diff --git a/Tests/TestSocketIOObjc/SocketObjectiveCTest.m b/Tests/TestSocketIOObjc/SocketObjectiveCTest.m index 6246701..908b2a5 100644 --- a/Tests/TestSocketIOObjc/SocketObjectiveCTest.m +++ b/Tests/TestSocketIOObjc/SocketObjectiveCTest.m @@ -7,36 +7,20 @@ // Merely tests whether the Objective-C api breaks // +#import "SocketObjectiveCTest.h" + @import Dispatch; @import Foundation; @import XCTest; @import SocketIO; -@interface SocketObjectiveCTest : XCTestCase - -@property SocketIOClient* socket; - -@end +// TODO Manager interface tests @implementation SocketObjectiveCTest -- (void)setUp { - [super setUp]; - NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"]; - self.socket = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"log": @NO, @"forcePolling": @YES}]; -} - - (void)testProperties { - NSURL* url = nil; - - url = self.socket.socketURL; - self.socket.forceNew = false; - self.socket.handleQueue = dispatch_get_main_queue(); self.socket.nsp = @"/objective-c"; - self.socket.reconnects = false; - self.socket.reconnectWait = 1; - if (self.socket.status == SocketIOClientStatusConnected) { } - if (self.socket.engine == NULL) { } + if (self.socket.status == SocketIOStatusConnected) { } } - (void)testOnSyntax { @@ -62,7 +46,7 @@ } - (void)testJoinNamespaceSyntax { - [self.socket joinNamespace:@"/objective-c"]; + [self.socket joinNamespace]; } - (void)testOnAnySyntax { @@ -74,10 +58,6 @@ }]; } -- (void)testReconnectSyntax { - [self.socket reconnect]; -} - - (void)testRemoveAllHandlersSyntax { [self.socket removeAllHandlers]; } @@ -94,15 +74,16 @@ [self.socket off:@"test"]; } -- (void)testSocketManager { - SocketClientManager* manager = [SocketClientManager sharedManager]; - [manager addSocket:self.socket labeledAs:@"test"]; - [manager removeSocketWithLabel:@"test"]; -} - - (void)testSSLSecurity { SSLSecurity* sec = [[SSLSecurity alloc] initWithUsePublicKeys:0]; sec = nil; } +- (void)setUp { + [super setUp]; + NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"]; + self.manager = [[SocketManager alloc] initWithSocketURL:url config:nil]; + self.socket = [self.manager defaultSocket]; +} + @end diff --git a/Usage Docs/FAQ.md b/Usage Docs/FAQ.md index 1090073..dc4f741 100644 --- a/Usage Docs/FAQ.md +++ b/Usage Docs/FAQ.md @@ -13,11 +13,11 @@ One of the most common reasons your event might not be called is if the client i Take this code for example: ```swift -class SocketManager { +class Manager { func addHandlers() { - let socket = SocketIOClient(socketURL: URL(string: "http://somesocketioserver.com")!) + let manager = SocketManager(socketURL: URL(string: "http://somesocketioserver.com")!) - socket.on("myEvent") {data, ack in + manager.defaultSocket.on("myEvent") {data, ack in print(data) } } @@ -25,30 +25,20 @@ class SocketManager { } ``` -This code is **incorrect**, and the event handler will never be called. Because as soon as this method is called `socket` -will be released and its memory reclaimed. +This code is **incorrect**, and the event handler will never be called. Because as soon as this method is called `manager` +will be released, along with the socket, and its memory reclaimed. A correct way would be: ```swift -class SocketManager { - let socket = SocketIOClient(socketURL: URL(string: "http://somesocketioserver.com")!) +class Manager { + let manager = SocketManager(socketURL: URL(string: "http://somesocketioserver.com")!) func addHandlers() { - socket.on("myEvent") {data, ack in + manager.defaultSocket.on("myEvent") {data, ack in print(data) } } } ``` - ------- - -Another case where this might happen is if you use namespaces in your socket.io application. - -In the JavaScript client a url that looks like `http://somesocketioserver.com/client` would be done with the `nsp` config. - -```swift -let socket = SocketIOClient(socketURL: URL(string: "http://somesocketioserver.com")!, config: [.nsp("/client")]) -```