diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 5186d070..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f795a08..38fadbb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v14.0.0 + +- Minimum version of the client is now Swift 4.2. +- Add exponential backoff for reconnects, with `reconnectWaitMax` and `randomizationFactor` options [#1149](https://github.com/socketio/socket.io-client-swift/pull/1149) +- `statusChange` event's data format adds a second value, the raw value of the status. This is for use in Objective-C. [#1147](https://github.com/socketio/socket.io-client-swift/issues/1147) + # v13.4.0 - Add emits with write completion handlers. [#1096](https://github.com/socketio/socket.io-client-swift/issues/1096) @@ -69,4 +75,3 @@ Important API changes - Adds `.sentPing` and `.gotPong` client events for tracking ping/pongs. - Makes the framework a single target. - Updates Starscream to 3.0 - diff --git a/Package.swift b/Package.swift index 3315dd43..d7a50903 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.0 +// swift-tools-version:4.2 import PackageDescription diff --git a/README.md b/README.md index eea6ed09..964c9813 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ SocketIOClient* socket = manager.defaultSocket; ## FAQS Checkout the [FAQs](https://nuclearace.github.io/Socket.IO-Client-Swift/faq.html) for commonly asked questions. -Checkout the [12to13](https://nuclearace.github.io/Socket.IO-Client-Swift/12to13.html) guide for migrating to v13. +Checkout the [12to13](https://nuclearace.github.io/Socket.IO-Client-Swift/12to13.html) guide for migrating to v13+ from v12 below. ## Installation @@ -76,7 +76,7 @@ If you need Swift 3.x use v11.1.3. ### Swift Package Manager Add the project as a dependency to your Package.swift: ```swift -// swift-tools-version:4.0 +// swift-tools-version:4.2 import PackageDescription @@ -86,7 +86,7 @@ let package = Package( .executable(name: "socket.io-test", targets: ["YourTargetName"]) ], dependencies: [ - .package(url: "https://github.com/socketio/socket.io-client-swift", .upToNextMinor(from: "13.3.0")) + .package(url: "https://github.com/socketio/socket.io-client-swift", .upToNextMinor(from: "14.0.0")) ], targets: [ .target(name: "YourTargetName", dependencies: ["SocketIO"], path: "./Path/To/Your/Sources") @@ -99,7 +99,7 @@ Then import `import SocketIO`. ### Carthage Add this line to your `Cartfile`: ``` -github "socketio/socket.io-client-swift" ~> 13.3.0 +github "socketio/socket.io-client-swift" ~> 14.0.0 ``` Run `carthage update --platform ios,macosx`. @@ -113,7 +113,7 @@ Create `Podfile` and add `pod 'Socket.IO-Client-Swift'`: use_frameworks! target 'YourApp' do - pod 'Socket.IO-Client-Swift', '~> 13.3.0' + pod 'Socket.IO-Client-Swift', '~> 14.0.0' end ``` diff --git a/Socket.IO-Client-Swift.podspec b/Socket.IO-Client-Swift.podspec index 6253e45e..b16f359a 100644 --- a/Socket.IO-Client-Swift.podspec +++ b/Socket.IO-Client-Swift.podspec @@ -21,9 +21,11 @@ Pod::Spec.new do |s| :tag => 'v13.4.0', :submodules => true } + + s.swift_version = "4.2" s.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.0' } s.source_files = "Source/SocketIO/**/*.swift", "Source/SocketIO/*.swift" - s.dependency "Starscream", "~> 3.0.2" + s.dependency "Starscream", "~> 3.0.6" end diff --git a/Socket.IO-Client-Swift.xcodeproj/project.pbxproj b/Socket.IO-Client-Swift.xcodeproj/project.pbxproj index cf7e6e25..f5b648dd 100644 --- a/Socket.IO-Client-Swift.xcodeproj/project.pbxproj +++ b/Socket.IO-Client-Swift.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1C6572803D7E252A77A86E5F /* SocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C65763817782DFAC67BE05C /* SocketManager.swift */; }; 1C6573B22DC9423CDFC32F05 /* SocketRawView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C657533E849FC3E4342C602 /* SocketRawView.swift */; }; + 1C657CDE5D510E8E2E573E39 /* utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C6577B639C34EE1C8829D9A /* utils.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 */; }; @@ -64,6 +65,7 @@ 1C6574AF9687A213814753E4 /* SocketManagerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketManagerSpec.swift; sourceTree = ""; }; 1C657533E849FC3E4342C602 /* SocketRawView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketRawView.swift; sourceTree = ""; }; 1C65763817782DFAC67BE05C /* SocketManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketManager.swift; sourceTree = ""; }; + 1C6577B639C34EE1C8829D9A /* utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = utils.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 = ""; }; @@ -159,6 +161,7 @@ 1C686BD71F869AF1007D8627 /* SocketParserTest.swift */, 1C686BD81F869AF1007D8627 /* SocketNamespacePacketTest.swift */, DD52BBAC5FAA7730D32CD5BF /* SocketMangerTest.swift */, + 1C6577B639C34EE1C8829D9A /* utils.swift */, ); name = TestSocketIO; path = Tests/TestSocketIO; @@ -501,6 +504,7 @@ 1C686BE81F869AFD007D8627 /* SocketNamespacePacketTest.swift in Sources */, DD52BCCD25EFA76E0F9B313C /* SocketMangerTest.swift in Sources */, DD52B53F2609D91A683DFCDD /* ManagerObjectiveCTest.m in Sources */, + 1C657CDE5D510E8E2E573E39 /* utils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Source/SocketIO/Client/SocketIOClient.swift b/Source/SocketIO/Client/SocketIOClient.swift index b05b0ecb..0e558a87 100644 --- a/Source/SocketIO/Client/SocketIOClient.swift +++ b/Source/SocketIO/Client/SocketIOClient.swift @@ -83,7 +83,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec { @objc public private(set) var status = SocketIOStatus.notConnected { didSet { - handleClientEvent(.statusChange, data: [status]) + handleClientEvent(.statusChange, data: [status, status.rawValue]) } } diff --git a/Source/SocketIO/Client/SocketIOClientOption.swift b/Source/SocketIO/Client/SocketIOClientOption.swift index 02d88989..1d687e0a 100644 --- a/Source/SocketIO/Client/SocketIOClientOption.swift +++ b/Source/SocketIO/Client/SocketIOClientOption.swift @@ -75,8 +75,14 @@ public enum SocketIOClientOption : ClientOption { /// The number of times to try and reconnect before giving up. Pass `-1` to [never give up](https://www.youtube.com/watch?v=dQw4w9WgXcQ). case reconnectAttempts(Int) - /// The number of seconds to wait before reconnect attempts. + /// The minimum number of seconds to wait before reconnect attempts. case reconnectWait(Int) + + /// The maximum number of seconds to wait before reconnect attempts. + case reconnectWaitMax(Int) + + /// The randomization factor for calculating reconnect jitter. + case randomizationFactor(Double) /// Set `true` if your server is using secure transports. case secure(Bool) @@ -125,6 +131,10 @@ public enum SocketIOClientOption : ClientOption { description = "reconnectAttempts" case .reconnectWait: description = "reconnectWait" + case .reconnectWaitMax: + description = "reconnectWaitMax" + case .randomizationFactor: + description = "randomizationFactor" case .secure: description = "secure" case .selfSigned: @@ -170,6 +180,10 @@ public enum SocketIOClientOption : ClientOption { value = attempts case let .reconnectWait(wait): value = wait + case let .reconnectWaitMax(wait): + value = wait + case let .randomizationFactor(factor): + value = factor case let .secure(secure): value = secure case let .security(security): diff --git a/Source/SocketIO/Client/SocketIOClientSpec.swift b/Source/SocketIO/Client/SocketIOClientSpec.swift index 17cee736..de9f31ab 100644 --- a/Source/SocketIO/Client/SocketIOClientSpec.swift +++ b/Source/SocketIO/Client/SocketIOClientSpec.swift @@ -327,6 +327,9 @@ public enum SocketClientEvent : String { /// Emitted every time there is a change in the client's status. /// + /// The payload for data is [SocketIOClientStatus, Int]. Where the second item is the raw value. Use the second one + /// if you are working in Objective-C. + /// /// Usage: /// /// ```swift diff --git a/Source/SocketIO/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift index e348eed3..ce3e9013 100644 --- a/Source/SocketIO/Manager/SocketManager.swift +++ b/Source/SocketIO/Manager/SocketManager.swift @@ -97,9 +97,15 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa /// 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. + /// The minimum number of seconds to wait before attempting to reconnect. public var reconnectWait = 10 + /// The maximum number of seconds to wait before attempting to reconnect. + public var reconnectWaitMax = 30 + + /// The randomization factor for calculating reconnect jitter. + public var randomizationFactor = 0.5 + /// The status of this manager. public private(set) var status: SocketIOStatus = .notConnected { didSet { @@ -474,7 +480,21 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa currentReconnectAttempt += 1 connect() - handleQueue.asyncAfter(deadline: DispatchTime.now() + Double(reconnectWait), execute: _tryReconnect) + let interval = reconnectInterval(attempts: currentReconnectAttempt) + DefaultSocketLogger.Logger.log("Scheduling reconnect in \(interval)s", type: SocketManager.logType) + handleQueue.asyncAfter(deadline: DispatchTime.now() + interval, execute: _tryReconnect) + } + + func reconnectInterval(attempts: Int) -> Double { + // apply exponential factor + let backoffFactor = pow(1.5, attempts) + let interval = Double(reconnectWait) * Double(truncating: backoffFactor as NSNumber) + // add in a random factor smooth thundering herds + let rand = Double.random(in: 0 ..< 1) + let randomFactor = rand * randomizationFactor * Double(truncating: interval as NSNumber) + // add in random factor, and clamp to min and max values + let combined = interval + randomFactor + return Double(fmax(Double(reconnectWait), fmin(combined, Double(reconnectWaitMax)))) } /// Sets manager specific configs. @@ -493,6 +513,10 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa self.reconnectAttempts = attempts case let .reconnectWait(wait): reconnectWait = abs(wait) + case let .reconnectWaitMax(wait): + reconnectWaitMax = abs(wait) + case let .randomizationFactor(factor): + randomizationFactor = factor case let .log(log): DefaultSocketLogger.Logger.log = log case let .logger(logger): diff --git a/Source/SocketIO/Manager/SocketManagerSpec.swift b/Source/SocketIO/Manager/SocketManagerSpec.swift index 4440193f..35d5afc2 100644 --- a/Source/SocketIO/Manager/SocketManagerSpec.swift +++ b/Source/SocketIO/Manager/SocketManagerSpec.swift @@ -69,8 +69,14 @@ public protocol SocketManagerSpec : AnyObject, SocketEngineClient { /// 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. + /// The minimum number of seconds to wait before attempting to reconnect. var reconnectWait: Int { get set } + + /// The maximum number of seconds to wait before attempting to reconnect. + var reconnectWaitMax: Int { get set } + + /// The randomization factor for calculating reconnect jitter. + var randomizationFactor: Double { get set } /// The URL of the socket.io server. var socketURL: URL { get } diff --git a/Source/SocketIO/Util/SocketExtensions.swift b/Source/SocketIO/Util/SocketExtensions.swift index fe9086fd..46363270 100644 --- a/Source/SocketIO/Util/SocketExtensions.swift +++ b/Source/SocketIO/Util/SocketExtensions.swift @@ -71,6 +71,10 @@ extension Dictionary where Key == String, Value == Any { return .reconnectAttempts(attempts) case let ("reconnectWait", wait as Int): return .reconnectWait(wait) + case let ("reconnectWaitMax", wait as Int): + return .reconnectWaitMax(wait) + case let ("randomizationFactor", factor as Double): + return .randomizationFactor(factor) case let ("secure", secure as Bool): return .secure(secure) case let ("security", security as SSLSecurity): diff --git a/Tests/TestSocketIO/SocketMangerTest.swift b/Tests/TestSocketIO/SocketMangerTest.swift index 80419482..453af2ea 100644 --- a/Tests/TestSocketIO/SocketMangerTest.swift +++ b/Tests/TestSocketIO/SocketMangerTest.swift @@ -15,6 +15,8 @@ class SocketMangerTest : XCTestCase { XCTAssertEqual(manager.handleQueue, DispatchQueue.main) XCTAssertTrue(manager.reconnects) XCTAssertEqual(manager.reconnectWait, 10) + XCTAssertEqual(manager.reconnectWaitMax, 30) + XCTAssertEqual(manager.randomizationFactor, 0.5) XCTAssertEqual(manager.status, .notConnected) } @@ -27,6 +29,21 @@ class SocketMangerTest : XCTestCase { XCTAssertEqual(manager.config.first!, .secure(true)) } + + func testBackoffIntervalCalulation() { + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: -1), Double(manager.reconnectWaitMax)) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 0), 15) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 1), 22.5) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 2), 33.75) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 50), Double(manager.reconnectWaitMax)) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 10000), Double(manager.reconnectWaitMax)) + + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: -1), Double(manager.reconnectWait)) + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 0), Double(manager.reconnectWait)) + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 1), 15) + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 2), 22.5) + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 10000), Double(manager.reconnectWait)) + } func testManagerCallsConnect() { setUpSockets() @@ -90,6 +107,8 @@ class SocketMangerTest : XCTestCase { .forceNew(true), .reconnects(false), .reconnectWait(5), + .reconnectWaitMax(5), + .randomizationFactor(0.7), .reconnectAttempts(5) ]) @@ -97,6 +116,8 @@ class SocketMangerTest : XCTestCase { XCTAssertTrue(manager.forceNew) XCTAssertFalse(manager.reconnects) XCTAssertEqual(manager.reconnectWait, 5) + XCTAssertEqual(manager.reconnectWaitMax, 5) + XCTAssertEqual(manager.randomizationFactor, 0.7) XCTAssertEqual(manager.reconnectAttempts, 5) } diff --git a/Tests/TestSocketIO/utils.swift b/Tests/TestSocketIO/utils.swift new file mode 100644 index 00000000..66d99acd --- /dev/null +++ b/Tests/TestSocketIO/utils.swift @@ -0,0 +1,13 @@ +// +// Created by Erik Little on 2019-01-11. +// + +import Foundation +@testable import SocketIO + +public class OBjcUtils: NSObject { + @objc + public static func setTestStatus(socket: SocketIOClient, status: SocketIOStatus) { + socket.setTestStatus(status) + } +} diff --git a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m index 393da4f2..a807eedd 100644 --- a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m +++ b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m @@ -34,6 +34,8 @@ - (void)testManagerProperties { XCTAssertEqual(self.manager.handleQueue, dispatch_get_main_queue()); XCTAssertTrue(self.manager.reconnects); XCTAssertEqual(self.manager.reconnectWait, 10); + XCTAssertEqual(self.manager.reconnectWaitMax, 30); + XCTAssertEqual(self.manager.randomizationFactor, 0.5); XCTAssertEqual(self.manager.status, SocketIOStatusNotConnected); } diff --git a/Tests/TestSocketIOObjc/SocketObjectiveCTest.m b/Tests/TestSocketIOObjc/SocketObjectiveCTest.m index 28c21b94..61412c12 100644 --- a/Tests/TestSocketIOObjc/SocketObjectiveCTest.m +++ b/Tests/TestSocketIOObjc/SocketObjectiveCTest.m @@ -7,6 +7,7 @@ // Merely tests whether the Objective-C api breaks // +#import "SocketIO_Tests-Swift.h" #import "SocketObjectiveCTest.h" @import Dispatch; @@ -73,11 +74,11 @@ - (void)testEmitWriteCompletionSyntax { - (void)testEmitWriteCompletion { XCTestExpectation* expect = [self expectationWithDescription:@"Write completion should be called"]; - + [self.socket emit:@"testEmit" with:@[@YES] completion:^{ [expect fulfill]; }]; - + [self waitForExpectationsWithTimeout:0.3 handler:nil]; } @@ -98,6 +99,19 @@ - (void)testSSLSecurity { sec = nil; } +- (void)testStatusChangeHandler { + XCTestExpectation* expect = [self expectationWithDescription:@"statusChange should be correctly called"]; + + [self.socket on:@"statusChange" callback:^(NSArray* data, SocketAckEmitter* ack) { + XCTAssertTrue([data[1] integerValue] == SocketIOStatusConnecting); + [expect fulfill]; + }]; + + [OBjcUtils setTestStatusWithSocket:self.socket status:SocketIOStatusConnecting]; + + [self waitForExpectationsWithTimeout:0.3 handler:nil]; +} + - (void)setUp { [super setUp]; NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"];