diff --git a/README.md b/README.md index 41383c7..03364ff 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Carthage ----------------- Add this line to your `Cartfile`: ``` -github "socketio/socket.io-client-swift" ~> 5.0.0 # Or latest version +github "socketio/socket.io-client-swift" ~> 5.1.0 # Or latest version ``` Run `carthage update --platform ios,macosx`. @@ -99,7 +99,7 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' use_frameworks! -pod 'Socket.IO-Client-Swift', '~> 5.0.0' # Or latest version +pod 'Socket.IO-Client-Swift', '~> 5.1.0' # Or latest version ``` Install pods: @@ -127,7 +127,7 @@ CocoaSeeds Add this line to your `Seedfile`: ``` -github "socketio/socket.io-client-swift", "v5.0.0", :files => "SocketIOClientSwift/*.swift" # Or latest version +github "socketio/socket.io-client-swift", "v5.1.0", :files => "SocketIOClientSwift/*.swift" # Or latest version ``` Run `seed install`. @@ -176,7 +176,7 @@ Methods 7. `emitWithAck(event: String, withItems items: [AnyObject]) -> (UInt64, (NSArray?) -> Void) -> Void` - `emitWithAck` for Objective-C. Note: The message is not sent until you call the returned function. 8. `connect()` - Establishes a connection to the server. A "connect" event is fired upon successful connection. 9. `connect(timeoutAfter timeoutAfter: Int, withTimeoutHandler handler: (() -> Void)?)` - Connect to the server. If it isn't connected after timeoutAfter seconds, the handler is called. -10. `close()` - Closes the socket. Once a socket is closed it should not be reopened. +10. `disconnect()` - Closes the socket. Reopening a disconnected socket is not fully tested. 11. `reconnect()` - Causes the client to reconnect to the server. 12. `joinNamespace(namespace: String)` - Causes the client to join namespace. Shouldn't need to be called unless you change namespaces manually. 13. `leaveNamespace()` - Causes the client to leave the nsp and go back to / diff --git a/Socket.IO-Client-Swift.podspec b/Socket.IO-Client-Swift.podspec index 9e238e1..9f5cd7b 100644 --- a/Socket.IO-Client-Swift.podspec +++ b/Socket.IO-Client-Swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "Socket.IO-Client-Swift" s.module_name = "SocketIOClientSwift" - s.version = "5.0.0" + s.version = "5.1.0" s.summary = "Socket.IO-client for iOS and OS X" s.description = <<-DESC Socket.IO-client for iOS and OS X. @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.ios.deployment_target = '8.0' s.osx.deployment_target = '10.10' s.tvos.deployment_target = '9.0' - s.source = { :git => "https://github.com/socketio/socket.io-client-swift.git", :tag => 'v5.0.0' } + s.source = { :git => "https://github.com/socketio/socket.io-client-swift.git", :tag => 'v5.1.0' } s.source_files = "Source/**/*.swift" s.requires_arc = true # s.dependency 'Starscream', '~> 0.9' # currently this repo includes Starscream swift files diff --git a/Socket.IO-Client-Swift.xcodeproj/project.pbxproj b/Socket.IO-Client-Swift.xcodeproj/project.pbxproj index 6053b25..88aa8d7 100644 --- a/Socket.IO-Client-Swift.xcodeproj/project.pbxproj +++ b/Socket.IO-Client-Swift.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 57634A2F1BD9B46D00E19CD7 /* SocketBasicPacketTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F124EF1BC574CF002966F4 /* SocketBasicPacketTest.swift */; }; 57634A321BD9B46D00E19CD7 /* SocketNamespacePacketTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7472C65B1BCAB53E003CA70D /* SocketNamespacePacketTest.swift */; }; 57634A3F1BD9B4BF00E19CD7 /* SocketIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57634A161BD9B46A00E19CD7 /* SocketIO.framework */; }; + 740CA1201C496EEB00CB98F4 /* SocketEngineWebsocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740CA11F1C496EEB00CB98F4 /* SocketEngineWebsocket.swift */; }; + 740CA1211C496EF200CB98F4 /* SocketEngineWebsocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740CA11F1C496EEB00CB98F4 /* SocketEngineWebsocket.swift */; }; + 740CA1221C496EF700CB98F4 /* SocketEngineWebsocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740CA11F1C496EEB00CB98F4 /* SocketEngineWebsocket.swift */; }; 74171E631C10CD240062D398 /* SocketAckEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74171E501C10CD240062D398 /* SocketAckEmitter.swift */; }; 74171E641C10CD240062D398 /* SocketAckEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74171E501C10CD240062D398 /* SocketAckEmitter.swift */; }; 74171E651C10CD240062D398 /* SocketAckEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74171E501C10CD240062D398 /* SocketAckEmitter.swift */; }; @@ -115,6 +118,9 @@ 74171ED41C10CD240062D398 /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74171E621C10CD240062D398 /* WebSocket.swift */; }; 741F39EE1BD025D80026C9CC /* SocketEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 741F39ED1BD025D80026C9CC /* SocketEngineTest.swift */; }; 741F39EF1BD025D80026C9CC /* SocketEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 741F39ED1BD025D80026C9CC /* SocketEngineTest.swift */; }; + 7420CB791C49629E00956AA4 /* SocketEnginePollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7420CB781C49629E00956AA4 /* SocketEnginePollable.swift */; }; + 7420CB7A1C49629E00956AA4 /* SocketEnginePollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7420CB781C49629E00956AA4 /* SocketEnginePollable.swift */; }; + 7420CB7B1C49629E00956AA4 /* SocketEnginePollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7420CB781C49629E00956AA4 /* SocketEnginePollable.swift */; }; 74321DCB1C2D939A00CF6F43 /* SocketAckManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74321DC91C2D939A00CF6F43 /* SocketAckManagerTest.swift */; }; 74321DCC1C2D939A00CF6F43 /* SocketParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74321DCA1C2D939A00CF6F43 /* SocketParserTest.swift */; }; 7471CCEA1C39926300364B59 /* SocketClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ABF7761C3991C10078C657 /* SocketClientSpec.swift */; }; @@ -165,6 +171,7 @@ 572EF2481B51F18A00EEBB58 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 57634A161BD9B46A00E19CD7 /* SocketIO.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SocketIO.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57634A3B1BD9B46D00E19CD7 /* SocketIO-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SocketIO-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 740CA11F1C496EEB00CB98F4 /* SocketEngineWebsocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SocketEngineWebsocket.swift; path = Source/SocketEngineWebsocket.swift; sourceTree = ""; }; 74171E501C10CD240062D398 /* SocketAckEmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SocketAckEmitter.swift; path = Source/SocketAckEmitter.swift; sourceTree = ""; }; 74171E511C10CD240062D398 /* SocketAckManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SocketAckManager.swift; path = Source/SocketAckManager.swift; sourceTree = ""; }; 74171E521C10CD240062D398 /* SocketAnyEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SocketAnyEvent.swift; path = Source/SocketAnyEvent.swift; sourceTree = ""; }; @@ -185,6 +192,7 @@ 74171E611C10CD240062D398 /* SwiftRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftRegex.swift; path = Source/SwiftRegex.swift; sourceTree = ""; }; 74171E621C10CD240062D398 /* WebSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WebSocket.swift; path = Source/WebSocket.swift; sourceTree = ""; }; 741F39ED1BD025D80026C9CC /* SocketEngineTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineTest.swift; sourceTree = ""; }; + 7420CB781C49629E00956AA4 /* SocketEnginePollable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SocketEnginePollable.swift; path = Source/SocketEnginePollable.swift; sourceTree = ""; }; 74321DC91C2D939A00CF6F43 /* SocketAckManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketAckManagerTest.swift; sourceTree = ""; }; 74321DCA1C2D939A00CF6F43 /* SocketParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketParserTest.swift; sourceTree = ""; }; 7472C65B1BCAB53E003CA70D /* SocketNamespacePacketTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketNamespacePacketTest.swift; sourceTree = ""; }; @@ -349,7 +357,9 @@ 74171E531C10CD240062D398 /* SocketEngine.swift */, 74171E541C10CD240062D398 /* SocketEngineClient.swift */, 74171E551C10CD240062D398 /* SocketEnginePacketType.swift */, + 7420CB781C49629E00956AA4 /* SocketEnginePollable.swift */, 74171E561C10CD240062D398 /* SocketEngineSpec.swift */, + 740CA11F1C496EEB00CB98F4 /* SocketEngineWebsocket.swift */, 74171E571C10CD240062D398 /* SocketEventHandler.swift */, 74171E581C10CD240062D398 /* SocketFixUTF8.swift */, 74171E591C10CD240062D398 /* SocketIOClient.swift */, @@ -599,10 +609,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 740CA1221C496EF700CB98F4 /* SocketEngineWebsocket.swift in Sources */, 74171E931C10CD240062D398 /* SocketFixUTF8.swift in Sources */, 74171EA51C10CD240062D398 /* SocketIOClientStatus.swift in Sources */, 74171E751C10CD240062D398 /* SocketEngine.swift in Sources */, 74171E691C10CD240062D398 /* SocketAckManager.swift in Sources */, + 7420CB791C49629E00956AA4 /* SocketEnginePollable.swift in Sources */, 74ABF7771C3991C10078C657 /* SocketClientSpec.swift in Sources */, 74171E871C10CD240062D398 /* SocketEngineSpec.swift in Sources */, 74171E631C10CD240062D398 /* SocketAckEmitter.swift in Sources */, @@ -656,10 +668,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 740CA1211C496EF200CB98F4 /* SocketEngineWebsocket.swift in Sources */, 7471CCEA1C39926300364B59 /* SocketClientSpec.swift in Sources */, 74171E951C10CD240062D398 /* SocketFixUTF8.swift in Sources */, 74171EA71C10CD240062D398 /* SocketIOClientStatus.swift in Sources */, 74171E771C10CD240062D398 /* SocketEngine.swift in Sources */, + 7420CB7A1C49629E00956AA4 /* SocketEnginePollable.swift in Sources */, 74171E6B1C10CD240062D398 /* SocketAckManager.swift in Sources */, 74171E891C10CD240062D398 /* SocketEngineSpec.swift in Sources */, 74171E651C10CD240062D398 /* SocketAckEmitter.swift in Sources */, @@ -697,10 +711,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 740CA1201C496EEB00CB98F4 /* SocketEngineWebsocket.swift in Sources */, 7471CCEB1C39926C00364B59 /* SocketClientSpec.swift in Sources */, 74171E971C10CD240062D398 /* SocketFixUTF8.swift in Sources */, 74171EA91C10CD240062D398 /* SocketIOClientStatus.swift in Sources */, 74171E791C10CD240062D398 /* SocketEngine.swift in Sources */, + 7420CB7B1C49629E00956AA4 /* SocketEnginePollable.swift in Sources */, 74171E6D1C10CD240062D398 /* SocketAckManager.swift in Sources */, 74171E8B1C10CD240062D398 /* SocketEngineSpec.swift in Sources */, 74171E671C10CD240062D398 /* SocketAckEmitter.swift in Sources */, diff --git a/Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-Mac.xcscheme b/Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-Mac.xcscheme index 7fef4ce..64047b1 100644 --- a/Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-Mac.xcscheme +++ b/Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-Mac.xcscheme @@ -15,7 +15,7 @@ @@ -57,7 +57,7 @@ @@ -79,7 +79,7 @@ @@ -97,7 +97,7 @@ diff --git a/Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-iOS.xcscheme b/Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-iOS.xcscheme index fc2fa75..e57f397 100644 --- a/Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-iOS.xcscheme +++ b/Socket.IO-Client-Swift.xcodeproj/xcshareddata/xcschemes/SocketIO-iOS.xcscheme @@ -15,7 +15,7 @@ @@ -58,7 +58,7 @@ @@ -80,7 +80,7 @@ @@ -98,7 +98,7 @@ diff --git a/SocketIO-MacTests/SocketEngineTest.swift b/SocketIO-MacTests/SocketEngineTest.swift index 53c7ec6..4572a0f 100644 --- a/SocketIO-MacTests/SocketEngineTest.swift +++ b/SocketIO-MacTests/SocketEngineTest.swift @@ -50,4 +50,28 @@ class SocketEngineTest: XCTestCase { engine.parsePollingMessage("15:42[\"blankTest\"]24:42[\"stringTest\",\"hello\"]") waitForExpectationsWithTimeout(3, handler: nil) } + + func testEngineDoesErrorOnUnknownTransport() { + let finalExpectation = expectationWithDescription("Unknown Transport") + + client.on("error") {data, ack in + if let error = data[0] as? String where error == "Unknown transport" { + finalExpectation.fulfill() + } + } + + engine.parseEngineMessage("{\"code\": 0, \"message\": \"Unknown transport\"}", fromPolling: false) + waitForExpectationsWithTimeout(3, handler: nil) + } + + func testEngineDoesErrorOnUnknownMessage() { + let finalExpectation = expectationWithDescription("Engine Errors") + + client.on("error") {data, ack in + finalExpectation.fulfill() + } + + engine.parseEngineMessage("afafafda", fromPolling: false) + waitForExpectationsWithTimeout(3, handler: nil) + } } diff --git a/SocketIO-MacTests/SocketSideEffectTest.swift b/SocketIO-MacTests/SocketSideEffectTest.swift index 9ffd043..e703335 100644 --- a/SocketIO-MacTests/SocketSideEffectTest.swift +++ b/SocketIO-MacTests/SocketSideEffectTest.swift @@ -100,6 +100,18 @@ class SocketSideEffectTest: XCTestCase { XCTAssertEqual(socket.testHandlers.count, 1) } + func testHandlesErrorPacket() { + let expectation = expectationWithDescription("Handled error") + socket.on("error") {data, ack in + if let error = data[0] as? String where error == "test error" { + expectation.fulfill() + } + } + + socket.parseSocketMessage("4\"test error\"") + waitForExpectationsWithTimeout(3, handler: nil) + } + func testHandleBinaryEvent() { let expectation = expectationWithDescription("handled binary event") socket.on("test") {data, ack in diff --git a/Source/SocketClientSpec.swift b/Source/SocketClientSpec.swift index a0314b4..6408f39 100644 --- a/Source/SocketClientSpec.swift +++ b/Source/SocketClientSpec.swift @@ -28,8 +28,16 @@ protocol SocketClientSpec: class { func didConnect() func didDisconnect(reason: String) - func didError(reason: AnyObject) + func didError(reason: String) func handleAck(ack: Int, data: [AnyObject]) func handleEvent(event: String, data: [AnyObject], isInternalMessage: Bool, withAck ack: Int) func joinNamespace(namespace: String) -} \ No newline at end of file +} + +extension SocketClientSpec { + func didError(reason: String) { + DefaultSocketLogger.Logger.error("%@", type: "SocketIOClient", args: reason) + + handleEvent("error", data: [reason], isInternalMessage: true, withAck: -1) + } +} diff --git a/Source/SocketEngine.swift b/Source/SocketEngine.swift index 5eb023e..666f8d4 100644 --- a/Source/SocketEngine.swift +++ b/Source/SocketEngine.swift @@ -24,12 +24,32 @@ import Foundation -public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { - public private(set) var sid = "" +public final class SocketEngine: NSObject, SocketEnginePollable, SocketEngineWebsocket { + public let emitQueue = dispatch_queue_create("com.socketio.engineEmitQueue", DISPATCH_QUEUE_SERIAL) + public let handleQueue = dispatch_queue_create("com.socketio.engineHandleQueue", DISPATCH_QUEUE_SERIAL) + public let parseQueue = dispatch_queue_create("com.socketio.engineParseQueue", DISPATCH_QUEUE_SERIAL) + + public var postWait = [String]() + public var waitingForPoll = false + public var waitingForPost = false + + public private(set) var closed = false + public private(set) var connected = false public private(set) var cookies: [NSHTTPCookie]? + public private(set) var extraHeaders: [String: String]? + public private(set) var fastUpgrade = false + public private(set) var forcePolling = false + public private(set) var forceWebsockets = false + public private(set) var invalidated = false + public private(set) var pingTimer: NSTimer? + public private(set) var polling = true + public private(set) var probing = false + public private(set) var session: NSURLSession? + public private(set) var sid = "" public private(set) var socketPath = "/engine.io" public private(set) var urlPolling = "" public private(set) var urlWebSocket = "" + public private(set) var websocket = false public private(set) var ws: WebSocket? public weak var client: SocketEngineClient? @@ -40,22 +60,11 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { private typealias ProbeWaitQueue = [Probe] private let allowedCharacterSet = NSCharacterSet(charactersInString: "!*'();:@&=+$,/?%#[]\" {}").invertedSet - private let emitQueue = dispatch_queue_create("com.socketio.engineEmitQueue", DISPATCH_QUEUE_SERIAL) - private let handleQueue = dispatch_queue_create("com.socketio.engineHandleQueue", DISPATCH_QUEUE_SERIAL) private let logType = "SocketEngine" - private let parseQueue = dispatch_queue_create("com.socketio.engineParseQueue", DISPATCH_QUEUE_SERIAL) private let url: String - private let workQueue = NSOperationQueue() - + private var connectParams: [String: AnyObject]? - private var closed = false - private var extraHeaders: [String: String]? - private var fastUpgrade = false - private var forcePolling = false - private var forceWebsockets = false - private var invalidated = false private var pingInterval: Double? - private var pingTimer: NSTimer? private var pingTimeout = 0.0 { didSet { pongsMissedMax = Int(pingTimeout / (pingInterval ?? 25)) @@ -63,19 +72,10 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { } private var pongsMissed = 0 private var pongsMissedMax = 0 - private var postWait = [String]() - private var probing = false private var probeWait = ProbeWaitQueue() private var secure = false private var selfSigned = false - private var session: NSURLSession? private var voipEnabled = false - private var waitingForPoll = false - private var waitingForPost = false - private var websocketConnected = false - private(set) var connected = false - private(set) var polling = true - private(set) var websocket = false public init(client: SocketEngineClient, url: String, options: Set) { self.client = client @@ -130,20 +130,19 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { switch code { case 0: // Unknown transport - logAndError(error) + didError(error) case 1: // Unknown sid. clear and retry connect - sid = "" - open(connectParams) + didError(error) case 2: // Bad handshake request - logAndError(error) + didError(error) case 3: // Bad request - logAndError(error) + didError(error) default: - logAndError(error) + didError(error) } } } catch { - logAndError("Got unknown error from server") + didError("Got unknown error from server \(msg)") } } @@ -163,37 +162,29 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { } } - public func close() { - DefaultSocketLogger.Logger.log("Engine is being closed.", type: logType) - - pingTimer?.invalidate() - closed = true - connected = false - - if websocket { - sendWebSocketMessage("", withType: .Close, withData: []) - } else { - sendPollMessage("", withType: .Close, withData: []) + public func close(reason: String) { + func postSendClose(data: NSData?, _ res: NSURLResponse?, _ err: NSError?) { + sid = "" + closed = true + invalidated = true + connected = false + + pingTimer?.invalidate() + ws?.disconnect() + stopPolling() + client?.engineDidClose(reason) } - ws?.disconnect() - stopPolling() - client?.engineDidClose("Disconnect") - } - - private func createBinaryDataForSend(data: NSData) -> Either { + DefaultSocketLogger.Logger.log("Engine is being closed.", type: logType) + if websocket { - var byteArray = [UInt8](count: 1, repeatedValue: 0x0) - byteArray[0] = 4 - let mutData = NSMutableData(bytes: &byteArray, length: 1) - - mutData.appendData(data) - - return .Left(mutData) + sendWebSocketMessage("", withType: .Close, withData: []) + postSendClose(nil, nil, nil) } else { - let str = "b4" + data.base64EncodedStringWithOptions(.Encoding64CharacterLineLength) - - return .Right(str) + // We need to take special care when we're polling that we send it ASAP + postWait.append(String(SocketEnginePacketType.Close.rawValue)) + let req = createRequestForPostWithPostWait() + doRequest(req, withCallback: postSendClose) } } @@ -263,8 +254,14 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { ws?.connect() } } + + public func didError(error: String) { + DefaultSocketLogger.Logger.error(error, type: logType) + client?.engineDidError(error) + close(error) + } - private func doFastUpgrade() { + public func doFastUpgrade() { if waitingForPoll { DefaultSocketLogger.Logger.error("Outstanding poll when switched to WebSockets," + "we'll probably disconnect soon. You should report this.", type: logType) @@ -293,6 +290,18 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { } } } + + // We had packets waiting for send when we upgraded + // Send them raw + public func flushWaitingForPostToWebSocket() { + guard let ws = self.ws else { return } + + for msg in postWait { + ws.writeString(fixDoubleUTF8(msg)) + } + + postWait.removeAll(keepCapacity: true) + } private func handleClose(reason: String) { client?.engineDidClose(reason) @@ -318,7 +327,7 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { connected = true if let upgrades = json?["upgrades"] as? [String] { - upgradeWs = upgrades.filter {$0 == "websocket"}.count != 0 + upgradeWs = upgrades.contains("websocket") } else { upgradeWs = false } @@ -331,17 +340,20 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { if !forcePolling && !forceWebsockets && upgradeWs { createWebsocketAndConnect(true) } + + + startPingTimer() + + if !forceWebsockets { + doPoll() + } + + client?.engineDidOpen?("Connect") } } catch { - DefaultSocketLogger.Logger.error("Error parsing open packet", type: logType) + didError("Error parsing open packet") return } - - startPingTimer() - - if !forceWebsockets { - doPoll() - } } private func handlePong(pongMessage: String) { @@ -353,34 +365,14 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { } } - // A poll failed, tell the client about it - private func handlePollingFailed(reason: String) { - connected = false - ws?.disconnect() - pingTimer?.invalidate() - waitingForPoll = false - waitingForPost = false - - if !closed { - client?.didError(reason) - client?.engineDidClose(reason) - } - } - - private func logAndError(error: String) { - DefaultSocketLogger.Logger.error(error, type: logType) - client?.didError(error) - } - - public func open(opts: [String: AnyObject]? = nil) { - connectParams = opts - + public func open(opts: [String: AnyObject]?) { if connected { - DefaultSocketLogger.Logger.error("Tried to open while connected", type: logType) - client?.didError("Tried to open engine while connected") - + DefaultSocketLogger.Logger.error("Engine tried opening while connected. This is probably a programming error. " + + "Abandoning open attempt", type: logType) return } + + connectParams = opts DefaultSocketLogger.Logger.log("Starting engine", type: logType) DefaultSocketLogger.Logger.log("Handshaking", type: logType) @@ -412,12 +404,12 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { doLongPoll(reqPolling) } - private func parseEngineData(data: NSData) { + public func parseEngineData(data: NSData) { DefaultSocketLogger.Logger.log("Got binary data: %@", type: "SocketEngine", args: data) client?.parseEngineBinaryData(data.subdataWithRange(NSMakeRange(1, data.length - 1))) } - private func parseEngineMessage(message: String, fromPolling: Bool) { + public func parseEngineMessage(message: String, fromPolling: Bool) { DefaultSocketLogger.Logger.log("Got message: %@", type: logType, args: message) let reader = SocketStringReader(message: message) @@ -452,13 +444,6 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { DefaultSocketLogger.Logger.log("Got unknown packet type", type: logType) } } - - private func probeWebSocket() { - if websocketConnected { - sendWebSocketMessage("probe", withType: .Ping, withData: []) - } - } - private func resetEngine() { closed = false @@ -469,12 +454,11 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { invalidated = false session = NSURLSession(configuration: .defaultSessionConfiguration(), delegate: sessionDelegate, - delegateQueue: workQueue) + delegateQueue: NSOperationQueue()) sid = "" waitingForPoll = false waitingForPost = false websocket = false - websocketConnected = false } /// Send an engine message (4) @@ -512,7 +496,7 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { } private func upgradeTransport() { - if websocketConnected { + if ws?.isConnected ?? false { DefaultSocketLogger.Logger.log("Upgrading transport to WebSockets", type: logType) fastUpgrade = true @@ -539,222 +523,9 @@ public final class SocketEngine: NSObject, SocketEngineSpec, WebSocketDelegate { } } } -} - -// Polling methods -extension SocketEngine { - private func addHeaders(req: NSMutableURLRequest) { - if cookies != nil { - let headers = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies!) - req.allHTTPHeaderFields = headers - } - - if extraHeaders != nil { - for (headerName, value) in extraHeaders! { - req.setValue(value, forHTTPHeaderField: headerName) - } - } - } - private func doPoll() { - if websocket || waitingForPoll || !connected || closed { - return - } - - waitingForPoll = true - let req = NSMutableURLRequest(URL: NSURL(string: urlPolling + "&sid=\(sid)&b64=1")!) - - addHeaders(req) - doLongPoll(req) - } - - private func doRequest(req: NSURLRequest, - withCallback callback: (NSData?, NSURLResponse?, NSError?) -> Void) { - if !polling || closed || invalidated { - DefaultSocketLogger.Logger.error("Tried to do polling request when not supposed to", type: logType) - return - } - - DefaultSocketLogger.Logger.log("Doing polling request", type: logType) - - session?.dataTaskWithRequest(req, completionHandler: callback).resume() - } - - private func doLongPoll(req: NSURLRequest) { - doRequest(req) {[weak self] data, res, err in - guard let this = self else {return} - - if err != nil || data == nil { - DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: this.logType) - - if this.polling { - this.handlePollingFailed(err?.localizedDescription ?? "Error") - } - - return - } - - DefaultSocketLogger.Logger.log("Got polling response", type: this.logType) - - if let str = String(data: data!, encoding: NSUTF8StringEncoding) { - dispatch_async(this.parseQueue) { - this.parsePollingMessage(str) - } - } - - this.waitingForPoll = false - - if this.fastUpgrade { - this.doFastUpgrade() - } else if !this.closed && this.polling { - this.doPoll() - } - } - } - - private func flushWaitingForPost() { - if postWait.count == 0 || !connected { - return - } else if websocket { - flushWaitingForPostToWebSocket() - return - } - - var postStr = "" - - for packet in postWait { - let len = packet.characters.count - - postStr += "\(len):\(packet)" - } - - postWait.removeAll(keepCapacity: false) - - let req = NSMutableURLRequest(URL: NSURL(string: urlPolling + "&sid=\(sid)")!) - - addHeaders(req) - - req.HTTPMethod = "POST" - req.setValue("text/plain; charset=UTF-8", forHTTPHeaderField: "Content-Type") - - let postData = postStr.dataUsingEncoding(NSUTF8StringEncoding, - allowLossyConversion: false)! - - req.HTTPBody = postData - req.setValue(String(postData.length), forHTTPHeaderField: "Content-Length") - - waitingForPost = true - - DefaultSocketLogger.Logger.log("POSTing: %@", type: logType, args: postStr) - - doRequest(req) {[weak self] data, res, err in - guard let this = self else {return} - - if err != nil { - DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: this.logType) - - if this.polling { - this.handlePollingFailed(err?.localizedDescription ?? "Error") - } - - return - } - - this.waitingForPost = false - - dispatch_async(this.emitQueue) { - if !this.fastUpgrade { - this.flushWaitingForPost() - this.doPoll() - } - } - } - } - - // We had packets waiting for send when we upgraded - // Send them raw - private func flushWaitingForPostToWebSocket() { - guard let ws = self.ws else { return } - - for msg in postWait { - ws.writeString(fixDoubleUTF8(msg)) - } - - postWait.removeAll(keepCapacity: true) - } - - func parsePollingMessage(str: String) { - guard str.characters.count != 1 else { - return - } - - var reader = SocketStringReader(message: str) - - while reader.hasNext { - if let n = Int(reader.readUntilStringOccurence(":")) { - let str = reader.read(n) - - dispatch_async(handleQueue) { - self.parseEngineMessage(str, fromPolling: true) - } - } else { - dispatch_async(handleQueue) { - self.parseEngineMessage(str, fromPolling: true) - } - break - } - } - } - - /// Send polling message. - /// Only call on emitQueue - private func sendPollMessage(message: String, withType type: SocketEnginePacketType, - withData datas: [NSData]) { - DefaultSocketLogger.Logger.log("Sending poll: %@ as type: %@", type: logType, args: message, type.rawValue) - let fixedMessage = doubleEncodeUTF8(message) - let strMsg = "\(type.rawValue)\(fixedMessage)" - - postWait.append(strMsg) - - for data in datas { - if case let .Right(bin) = createBinaryDataForSend(data) { - postWait.append(bin) - } - } - - if !waitingForPost { - flushWaitingForPost() - } - } - - private func stopPolling() { - invalidated = true - session?.finishTasksAndInvalidate() - } -} - -// WebSocket methods -extension SocketEngine { - /// Send message on WebSockets - /// Only call on emitQueue - private func sendWebSocketMessage(str: String, withType type: SocketEnginePacketType, - withData datas: [NSData]) { - DefaultSocketLogger.Logger.log("Sending ws: %@ as type: %@", type: logType, args: str, type.rawValue) - - ws?.writeString("\(type.rawValue)\(str)") - - for data in datas { - if case let .Left(bin) = createBinaryDataForSend(data) { - ws?.writeData(bin) - } - } - } - - // Delagate methods - - public func websocketDidConnect(socket:WebSocket) { - websocketConnected = true - + // Delegate methods + public func websocketDidConnect(socket: WebSocket) { if !forceWebsockets { probing = true probeWebSocket() @@ -766,7 +537,6 @@ extension SocketEngine { } public func websocketDidDisconnect(socket: WebSocket, error: NSError?) { - websocketConnected = false probing = false if closed { @@ -782,7 +552,7 @@ extension SocketEngine { let reason = error?.localizedDescription ?? "Socket Disconnected" if error != nil { - client?.didError(reason) + didError(reason) } client?.engineDidClose(reason) @@ -790,12 +560,4 @@ extension SocketEngine { flushProbeWait() } } - - public func websocketDidReceiveMessage(socket: WebSocket, text: String) { - parseEngineMessage(text, fromPolling: false) - } - - public func websocketDidReceiveData(socket: WebSocket, data: NSData) { - parseEngineData(data) - } } diff --git a/Source/SocketEngineClient.swift b/Source/SocketEngineClient.swift index 776dc5a..a1db7f6 100644 --- a/Source/SocketEngineClient.swift +++ b/Source/SocketEngineClient.swift @@ -25,9 +25,10 @@ import Foundation -@objc public protocol SocketEngineClient { - func didError(reason: AnyObject) +@objc public protocol SocketEngineClient { + func engineDidError(reason: String) func engineDidClose(reason: String) + optional func engineDidOpen(reason: String) func parseEngineMessage(msg: String) func parseEngineBinaryData(data: NSData) } diff --git a/Source/SocketEnginePollable.swift b/Source/SocketEnginePollable.swift new file mode 100644 index 0000000..cd8e512 --- /dev/null +++ b/Source/SocketEnginePollable.swift @@ -0,0 +1,231 @@ +// +// SocketEnginePollable.swift +// Socket.IO-Client-Swift +// +// Created by Erik Little on 1/15/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 + +/// Protocol that is used to implement socket.io polling support +public protocol SocketEnginePollable: SocketEngineSpec { + var invalidated: Bool { get } + /// Holds strings waiting to be sent over polling. + /// You shouldn't need to mess with this. + var postWait: [String] { get set } + var session: NSURLSession? { get } + /// Because socket.io doesn't let you send two polling request at the same time + /// we have to keep track if there's an outstanding poll + var waitingForPoll: Bool { get set } + /// Because socket.io doesn't let you send two post request at the same time + /// we have to keep track if there's an outstanding post + var waitingForPost: Bool { get set } + + func doPoll() + func sendPollMessage(message: String, withType type: SocketEnginePacketType, withData datas: [NSData]) + func stopPolling() +} + +// Default polling methods +extension SocketEnginePollable { + private func addHeaders(req: NSMutableURLRequest) { + if cookies != nil { + let headers = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies!) + req.allHTTPHeaderFields = headers + } + + if extraHeaders != nil { + for (headerName, value) in extraHeaders! { + req.setValue(value, forHTTPHeaderField: headerName) + } + } + } + + func createRequestForPostWithPostWait() -> NSURLRequest { + var postStr = "" + + for packet in postWait { + let len = packet.characters.count + + postStr += "\(len):\(packet)" + } + + DefaultSocketLogger.Logger.log("Created POST string: %@", type: "SocketEnginePolling", args: postStr) + + postWait.removeAll(keepCapacity: false) + + let req = NSMutableURLRequest(URL: NSURL(string: urlPolling + "&sid=\(sid)")!) + + addHeaders(req) + + req.HTTPMethod = "POST" + req.setValue("text/plain; charset=UTF-8", forHTTPHeaderField: "Content-Type") + + let postData = postStr.dataUsingEncoding(NSUTF8StringEncoding, + allowLossyConversion: false)! + + req.HTTPBody = postData + req.setValue(String(postData.length), forHTTPHeaderField: "Content-Length") + + return req + } + + public func doPoll() { + if websocket || waitingForPoll || !connected || closed { + return + } + + waitingForPoll = true + let req = NSMutableURLRequest(URL: NSURL(string: urlPolling + "&sid=\(sid)&b64=1")!) + + addHeaders(req) + doLongPoll(req) + } + + func doRequest(req: NSURLRequest, withCallback callback: (NSData?, NSURLResponse?, NSError?) -> Void) { + if !polling || closed || invalidated { + DefaultSocketLogger.Logger.error("Tried to do polling request when not supposed to", type: "SocketEnginePolling") + return + } + + DefaultSocketLogger.Logger.log("Doing polling request", type: "SocketEnginePolling") + + session?.dataTaskWithRequest(req, completionHandler: callback).resume() + } + + func doLongPoll(req: NSURLRequest) { + doRequest(req) {[weak self] data, res, err in + guard let this = self else {return} + + if err != nil || data == nil { + DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: "SocketEnginePolling") + + if this.polling { + this.didError(err?.localizedDescription ?? "Error") + } + + return + } + + DefaultSocketLogger.Logger.log("Got polling response", type: "SocketEnginePolling") + + if let str = String(data: data!, encoding: NSUTF8StringEncoding) { + dispatch_async(this.parseQueue) { + this.parsePollingMessage(str) + } + } + + this.waitingForPoll = false + + if this.fastUpgrade { + this.doFastUpgrade() + } else if !this.closed && this.polling { + this.doPoll() + } + } + } + + private func flushWaitingForPost() { + if postWait.count == 0 || !connected { + return + } else if websocket { + flushWaitingForPostToWebSocket() + return + } + + let req = createRequestForPostWithPostWait() + + waitingForPost = true + + DefaultSocketLogger.Logger.log("POSTing", type: "SocketEnginePolling") + + doRequest(req) {[weak self] data, res, err in + guard let this = self else { return } + + if err != nil { + DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: "SocketEnginePolling") + + if this.polling { + this.didError(err?.localizedDescription ?? "Error") + } + + return + } + + this.waitingForPost = false + + dispatch_async(this.emitQueue) { + if !this.fastUpgrade { + this.flushWaitingForPost() + this.doPoll() + } + } + } + } + + func parsePollingMessage(str: String) { + guard str.characters.count != 1 else { + return + } + + var reader = SocketStringReader(message: str) + + while reader.hasNext { + if let n = Int(reader.readUntilStringOccurence(":")) { + let str = reader.read(n) + + dispatch_async(handleQueue) { + self.parseEngineMessage(str, fromPolling: true) + } + } else { + dispatch_async(handleQueue) { + self.parseEngineMessage(str, fromPolling: true) + } + break + } + } + } + + /// Send polling message. + /// Only call on emitQueue + public func sendPollMessage(message: String, withType type: SocketEnginePacketType, withData datas: [NSData]) { + DefaultSocketLogger.Logger.log("Sending poll: %@ as type: %@", type: "SocketEnginePolling", args: message, type.rawValue) + let fixedMessage = doubleEncodeUTF8(message) + let strMsg = "\(type.rawValue)\(fixedMessage)" + + postWait.append(strMsg) + + for data in datas { + if case let .Right(bin) = createBinaryDataForSend(data) { + postWait.append(bin) + } + } + + if !waitingForPost { + flushWaitingForPost() + } + } + + public func stopPolling() { + waitingForPoll = false + waitingForPost = false + session?.finishTasksAndInvalidate() + } +} diff --git a/Source/SocketEngineSpec.swift b/Source/SocketEngineSpec.swift index 52ec6e5..a12c323 100644 --- a/Source/SocketEngineSpec.swift +++ b/Source/SocketEngineSpec.swift @@ -26,17 +26,53 @@ import Foundation @objc public protocol SocketEngineSpec { - weak var client: SocketEngineClient? {get set} - var cookies: [NSHTTPCookie]? {get} - var sid: String {get} - var socketPath: String {get} - var urlPolling: String {get} - var urlWebSocket: String {get} + weak var client: SocketEngineClient? { get set } + var closed: Bool { get } + var connected: Bool { get } + var cookies: [NSHTTPCookie]? { get } + var extraHeaders: [String: String]? { get } + var fastUpgrade: Bool { get } + var forcePolling: Bool { get } + var forceWebsockets: Bool { get } + var parseQueue: dispatch_queue_t! { get } + var pingTimer: NSTimer? { get } + var polling: Bool { get } + var probing: Bool { get } + var emitQueue: dispatch_queue_t! { get } + var handleQueue: dispatch_queue_t! { get } + var sid: String { get } + var socketPath: String { get } + var urlPolling: String { get } + var urlWebSocket: String { get } + var websocket: Bool { get } init(client: SocketEngineClient, url: String, options: NSDictionary?) - func close() + func close(reason: String) + func didError(error: String) + func doFastUpgrade() + func flushWaitingForPostToWebSocket() func open(opts: [String: AnyObject]?) + func parseEngineData(data: NSData) + func parseEngineMessage(message: String, fromPolling: Bool) func send(msg: String, withData datas: [NSData]) func write(msg: String, withType type: SocketEnginePacketType, withData data: [NSData]) } + +extension SocketEngineSpec { + func createBinaryDataForSend(data: NSData) -> Either { + if websocket { + var byteArray = [UInt8](count: 1, repeatedValue: 0x0) + byteArray[0] = 4 + let mutData = NSMutableData(bytes: &byteArray, length: 1) + + mutData.appendData(data) + + return .Left(mutData) + } else { + let str = "b4" + data.base64EncodedStringWithOptions(.Encoding64CharacterLineLength) + + return .Right(str) + } + } +} diff --git a/Source/SocketEngineWebsocket.swift b/Source/SocketEngineWebsocket.swift new file mode 100644 index 0000000..4b1be7f --- /dev/null +++ b/Source/SocketEngineWebsocket.swift @@ -0,0 +1,64 @@ +// +// SocketEngineWebsocket.swift +// Socket.IO-Client-Swift +// +// Created by Erik Little on 1/15/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 + +/// Protocol that is used to implement socket.io WebSocket support +public protocol SocketEngineWebsocket: SocketEngineSpec, WebSocketDelegate { + var ws: WebSocket? { get } + + func sendWebSocketMessage(str: String, withType type: SocketEnginePacketType, withData datas: [NSData]) +} + +// WebSocket methods +extension SocketEngineWebsocket { + func probeWebSocket() { + if ws?.isConnected ?? false { + sendWebSocketMessage("probe", withType: .Ping, withData: []) + } + } + + /// Send message on WebSockets + /// Only call on emitQueue + public func sendWebSocketMessage(str: String, withType type: SocketEnginePacketType, withData datas: [NSData]) { + DefaultSocketLogger.Logger.log("Sending ws: %@ as type: %@", type: "SocketEngine", args: str, type.rawValue) + + ws?.writeString("\(type.rawValue)\(str)") + + for data in datas { + if case let .Left(bin) = createBinaryDataForSend(data) { + ws?.writeData(bin) + } + } + } + + public func websocketDidReceiveMessage(socket: WebSocket, text: String) { + parseEngineMessage(text, fromPolling: false) + } + + public func websocketDidReceiveData(socket: WebSocket, data: NSData) { + parseEngineData(data) + } +} diff --git a/Source/SocketIOClient.swift b/Source/SocketIOClient.swift index 0db5c3c..4f41ed2 100644 --- a/Source/SocketIOClient.swift +++ b/Source/SocketIOClient.swift @@ -26,10 +26,10 @@ import Foundation public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable { public let socketURL: String - + public private(set) var engine: SocketEngineSpec? public private(set) var status = SocketIOClientStatus.NotConnected - + public var connectParams: [String: AnyObject]? public var forceNew = false public var nsp = "/" @@ -39,35 +39,35 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable public var sid: String? { return engine?.sid } - + private let emitQueue = dispatch_queue_create("com.socketio.emitQueue", DISPATCH_QUEUE_SERIAL) private let logType = "SocketIOClient" private let parseQueue = dispatch_queue_create("com.socketio.parseQueue", DISPATCH_QUEUE_SERIAL) - + private var anyHandler: ((SocketAnyEvent) -> Void)? private var currentReconnectAttempt = 0 private var handlers = [SocketEventHandler]() private var reconnectTimer: NSTimer? private var ackHandlers = SocketAckManager() - + private(set) var currentAck = -1 private(set) var handleQueue = dispatch_get_main_queue() private(set) var reconnectAttempts = -1 - + var waitingData = [SocketPacket]() - + /** Type safe way to create a new SocketIOClient. opts can be omitted */ public init(socketURL: String, options: Set = []) { self.options = options - + if socketURL["https://"].matches().count != 0 { self.options.insertIgnore(.Secure(true)) } - + self.socketURL = socketURL["https?://"] ~= "" - + for option in options { switch option { case let .ConnectParams(params): @@ -92,12 +92,12 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable continue } } - + self.options.insertIgnore(.Path("/socket.io")) - + super.init() } - + /** Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity. If using Swift it's recommended to use `init(var socketURL: String, opts: SocketOptionsDictionary? = nil)` @@ -106,92 +106,85 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable self.init(socketURL: socketURL, options: options?.toSocketOptionsSet() ?? []) } - + deinit { DefaultSocketLogger.Logger.log("Client is being deinit", type: logType) - engine?.close() + engine?.close("Client Deinit") } - + private func addEngine() -> SocketEngineSpec { DefaultSocketLogger.Logger.log("Adding engine", type: logType) - + engine = SocketEngine(client: self, url: socketURL, options: options) - + return engine! } - + private func clearReconnectTimer() { reconnectTimer?.invalidate() reconnectTimer = nil } - - /** - Closes the socket. Only reopen the same socket if you know what you're doing. - Will turn off automatic reconnects. - Pass true to fast if you're closing from a background task - */ + + @available(*, deprecated=6.0) public func close() { - DefaultSocketLogger.Logger.log("Closing socket", type: logType) - - reconnects = false - didDisconnect("Closed") + disconnect() } - + /** Connect to the server. */ public func connect() { connect(timeoutAfter: 0, withTimeoutHandler: nil) } - + /** Connect to the server. If we aren't connected after timeoutAfter, call handler */ public func connect(timeoutAfter timeoutAfter: Int, withTimeoutHandler handler: (() -> Void)?) { assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)") - + guard status != .Connected else { DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", type: logType) return } - + status = .Connecting - + if engine == nil || forceNew { addEngine().open(connectParams) } else { engine?.open(connectParams) } - + guard timeoutAfter != 0 else { return } - + let time = dispatch_time(DISPATCH_TIME_NOW, Int64(timeoutAfter) * Int64(NSEC_PER_SEC)) - + dispatch_after(time, handleQueue) {[weak self] in if let this = self where this.status != .Connected { this.status = .Closed - this.engine?.close() - + this.engine?.close("Connect timeout") + handler?() } } } - + private func createOnAck(items: [AnyObject]) -> OnAckCallback { currentAck += 1 - + return {[weak self, ack = currentAck] timeout, callback in if let this = self { this.ackHandlers.addAck(ack, callback: callback) - + dispatch_async(this.emitQueue) { this._emit(items, ack: ack) } - + if timeout != 0 { let time = dispatch_time(DISPATCH_TIME_NOW, Int64(timeout * NSEC_PER_SEC)) - + dispatch_after(time, this.handleQueue) { this.ackHandlers.timeoutAck(ack) } @@ -199,55 +192,51 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable } } } - + func didConnect() { DefaultSocketLogger.Logger.log("Socket connected", type: logType) status = .Connected currentReconnectAttempt = 0 clearReconnectTimer() - + // Don't handle as internal because something crazy could happen where // we disconnect before it's handled handleEvent("connect", data: [], isInternalMessage: false) } - + func didDisconnect(reason: String) { guard status != .Closed else { return } - + DefaultSocketLogger.Logger.log("Disconnected: %@", type: logType, args: reason) - + status = .Closed reconnects = false - + // Make sure the engine is actually dead. - engine?.close() + engine?.close("Client closed") handleEvent("disconnect", data: [reason], isInternalMessage: true) } - - /// error - public func didError(reason: AnyObject) { - DefaultSocketLogger.Logger.error("%@", type: logType, args: reason) - - handleEvent("error", data: reason as? [AnyObject] ?? [reason], - isInternalMessage: true) - } - + /** - Same as close + Closes the socket. Only reopen the same socket if you know what you're doing. + Will turn off automatic reconnects. */ public func disconnect() { - close() + DefaultSocketLogger.Logger.log("Closing socket", type: logType) + + reconnects = false + didDisconnect("Closed") } - + /** Send a message to the server */ public func emit(event: String, _ items: AnyObject...) { emit(event, withItems: items) } - + /** Same as emit, but meant for Objective-C */ @@ -256,12 +245,12 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable handleEvent("error", data: ["Tried emitting \(event) when not connected"], isInternalMessage: true) return } - + dispatch_async(emitQueue) { self._emit([event] + items) } } - + /** Sends a message to the server, requesting an ack. Use the onAck method of SocketAckHandler to add an ack. @@ -269,45 +258,45 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable public func emitWithAck(event: String, _ items: AnyObject...) -> OnAckCallback { return emitWithAck(event, withItems: items) } - + /** Same as emitWithAck, but for Objective-C */ public func emitWithAck(event: String, withItems items: [AnyObject]) -> OnAckCallback { return createOnAck([event] + items) } - + private func _emit(data: [AnyObject], ack: Int? = nil) { guard status == .Connected else { handleEvent("error", data: ["Tried emitting when not connected"], isInternalMessage: true) return } - + let packet = SocketPacket.packetFromEmit(data, id: ack ?? -1, nsp: nsp, ack: false) let str = packet.packetString - + DefaultSocketLogger.Logger.log("Emitting: %@", type: logType, args: str) - + engine?.send(str, withData: packet.binary) } - + // If the server wants to know that the client received data func emitAck(ack: Int, withItems items: [AnyObject]) { dispatch_async(emitQueue) { if self.status == .Connected { let packet = SocketPacket.packetFromEmit(items, id: ack ?? -1, nsp: self.nsp, ack: true) let str = packet.packetString - + DefaultSocketLogger.Logger.log("Emitting Ack: %@", type: self.logType, args: str) - + self.engine?.send(str, withData: packet.binary) } } } - + public func engineDidClose(reason: String) { waitingData.removeAll() - + if status == .Closed || !reconnects { didDisconnect(reason) } else if status != .Reconnecting { @@ -316,16 +305,23 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable tryReconnect() } } - + + /// error + public func engineDidError(reason: String) { + DefaultSocketLogger.Logger.error("%@", type: logType, args: reason) + + handleEvent("error", data: [reason], isInternalMessage: true) + } + // Called when the socket gets an ack for something it sent func handleAck(ack: Int, data: [AnyObject]) { guard status == .Connected else {return} - + DefaultSocketLogger.Logger.log("Handling ack: %@ with data: %@", type: logType, args: ack, data ?? "") - + ackHandlers.executeAck(ack, items: data) } - + /** Causes an event to be handled. Only use if you know what you're doing. */ @@ -333,18 +329,18 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable guard status == .Connected || isInternalMessage else { return } - + DefaultSocketLogger.Logger.log("Handling event: %@ with data: %@", type: logType, args: event, data ?? "") - + dispatch_async(handleQueue) { self.anyHandler?(SocketAnyEvent(event: event, items: data)) - + for handler in self.handlers where handler.event == event { handler.executeCallback(data, withAck: ack, withSocket: self) } } } - + /** Leaves nsp and goes back to / */ @@ -354,105 +350,105 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable nsp = "/" } } - + /** Joins namespace */ public func joinNamespace(namespace: String) { nsp = namespace - + if nsp != "/" { DefaultSocketLogger.Logger.log("Joining namespace", type: logType) engine?.send("0\(nsp)", withData: []) } } - + /** Removes handler(s) */ public func off(event: String) { DefaultSocketLogger.Logger.log("Removing handler for event: %@", type: logType, args: event) - + handlers = handlers.filter { $0.event != event } } - + /** Removes a handler with the specified UUID gotten from an `on` or `once` */ public func off(id id: NSUUID) { DefaultSocketLogger.Logger.log("Removing handler with id: %@", type: logType, args: id) - + handlers = handlers.filter { $0.id != id } } - + /** Adds a handler for an event. Returns: A unique id for the handler */ public func on(event: String, callback: NormalCallback) -> NSUUID { DefaultSocketLogger.Logger.log("Adding handler for event: %@", type: logType, args: event) - + let handler = SocketEventHandler(event: event, id: NSUUID(), callback: callback) handlers.append(handler) - + return handler.id } - + /** Adds a single-use handler for an event. Returns: A unique id for the handler */ public func once(event: String, callback: NormalCallback) -> NSUUID { DefaultSocketLogger.Logger.log("Adding once handler for event: %@", type: logType, args: event) - + let id = NSUUID() - + let handler = SocketEventHandler(event: event, id: id) {[weak self] data, ack in guard let this = self else {return} this.off(id: id) callback(data, ack) } - + handlers.append(handler) - + return handler.id } - + /** Adds a handler that will be called on every event. */ public func onAny(handler: (SocketAnyEvent) -> Void) { anyHandler = handler } - + /** Same as connect */ public func open() { connect() } - + public func parseEngineMessage(msg: String) { DefaultSocketLogger.Logger.log("Should parse message: %@", type: "SocketIOClient", args: msg) - + dispatch_async(parseQueue) { self.parseSocketMessage(msg) } } - + public func parseEngineBinaryData(data: NSData) { dispatch_async(parseQueue) { self.parseBinaryData(data) } } - + /** Tries to reconnect to the server. */ public func reconnect() { tryReconnect() } - + /** Removes all handlers. Can be used after disconnecting to break any potential remaining retain cycles. @@ -460,38 +456,38 @@ public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable public func removeAllHandlers() { handlers.removeAll(keepCapacity: false) } - + private func tryReconnect() { if reconnectTimer == nil { DefaultSocketLogger.Logger.log("Starting reconnect", type: logType) - + status = .Reconnecting - + dispatch_async(dispatch_get_main_queue()) { self.reconnectTimer = NSTimer.scheduledTimerWithTimeInterval(Double(self.reconnectWait), target: self, selector: "_tryReconnect", userInfo: nil, repeats: true) } } } - + @objc private func _tryReconnect() { if status == .Connected { clearReconnectTimer() - + return } - + if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts || !reconnects { clearReconnectTimer() didDisconnect("Reconnect Failed") - + return } - + DefaultSocketLogger.Logger.log("Trying to reconnect", type: logType) handleEvent("reconnectAttempt", data: [reconnectAttempts - currentReconnectAttempt], isInternalMessage: true) - + currentReconnectAttempt += 1 connect() } @@ -502,15 +498,15 @@ extension SocketIOClient { var testHandlers: [SocketEventHandler] { return handlers } - + func setTestable() { status = .Connected } - + func setTestEngine(engine: SocketEngineSpec?) { self.engine = engine } - + func emitTest(event: String, _ data: AnyObject...) { self._emit([event] + data) } diff --git a/Source/SocketParsable.swift b/Source/SocketParsable.swift index 35117ef..f7304c4 100644 --- a/Source/SocketParsable.swift +++ b/Source/SocketParsable.swift @@ -58,7 +58,7 @@ extension SocketParsable { case .Disconnect: didDisconnect("Got Disconnect") case .Error: - didError(pack.data) + handleEvent("error", data: pack.data, isInternalMessage: false, withAck: pack.id) default: DefaultSocketLogger.Logger.log("Got invalid packet: %@", type: "SocketParser", args: pack.description) }