Skip to content

Commit 5090f2a

Browse files
Merge pull request #294 from pusher/feature/222-respect-custom-closure-codes
Respect Pusher Channels Protocol WebSocket closure codes
2 parents 5d29233 + b8f47f4 commit 5090f2a

File tree

12 files changed

+573
-132
lines changed

12 files changed

+573
-132
lines changed

.swiftlint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ identifier_name:
1313

1414
# This generates a compiler error if more than this many SwiftLint warnings are present
1515
# (This threshold can become more restrictive as remaining warnings are resolved via refactoring)
16-
warning_threshold: 22
16+
warning_threshold: 21

PusherSwift.xcodeproj/project.pbxproj

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@
3232
33C40CB91C1DFC9C008A54E3 /* PusherSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 33831CD61A9CFFF200B124F1 /* PusherSwift.h */; settings = {ATTRIBUTES = (Public, ); }; };
3333
53140355250A8F7600F3D7BF /* PusherChannelsProtocolCloseCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53140354250A8F7600F3D7BF /* PusherChannelsProtocolCloseCode.swift */; };
3434
53140356250A8F7600F3D7BF /* PusherChannelsProtocolCloseCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53140354250A8F7600F3D7BF /* PusherChannelsProtocolCloseCode.swift */; };
35+
53140358250B8A9F00F3D7BF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53140357250B8A9F00F3D7BF /* WebSocket.swift */; };
36+
53140359250B8A9F00F3D7BF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53140357250B8A9F00F3D7BF /* WebSocket.swift */; };
37+
536F96B8252C841000D2C2F4 /* WebSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536F96B7252C841000D2C2F4 /* WebSocketTests.swift */; };
38+
536F96B9252C841000D2C2F4 /* WebSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536F96B7252C841000D2C2F4 /* WebSocketTests.swift */; };
3539
539D9AFC2507F67300B5765A /* PusherLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539D9AFB2507F67300B5765A /* PusherLogger.swift */; };
3640
539D9AFE2507F68400B5765A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539D9AFD2507F68400B5765A /* Constants.swift */; };
3741
539D9AFF2507F69400B5765A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539D9AFD2507F68400B5765A /* Constants.swift */; };
3842
539D9B002507F69B00B5765A /* PusherLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539D9AFB2507F67300B5765A /* PusherLogger.swift */; };
39-
539D9B022509101E00B5765A /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539D9B012509101E00B5765A /* WebSocket.swift */; };
40-
539D9B032509101E00B5765A /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539D9B012509101E00B5765A /* WebSocket.swift */; };
4143
539D9B05250913B300B5765A /* WebSocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539D9B04250913B300B5765A /* WebSocketConnection.swift */; };
4244
539D9B06250913B300B5765A /* WebSocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539D9B04250913B300B5765A /* WebSocketConnection.swift */; };
4345
736E53F5242A378B0052CC1B /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 736E53F3242A35D90052CC1B /* String+Extensions.swift */; };
@@ -146,9 +148,10 @@
146148
33BB99671D21226C00B25C2A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Tests/Info.plist; sourceTree = "<group>"; };
147149
33C1FD6E1D81BFC300921AD7 /* ObjectiveC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectiveC.swift; sourceTree = "<group>"; };
148150
53140354250A8F7600F3D7BF /* PusherChannelsProtocolCloseCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PusherChannelsProtocolCloseCode.swift; sourceTree = "<group>"; };
151+
53140357250B8A9F00F3D7BF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
152+
536F96B7252C841000D2C2F4 /* WebSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketTests.swift; sourceTree = "<group>"; };
149153
539D9AFB2507F67300B5765A /* PusherLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PusherLogger.swift; sourceTree = "<group>"; };
150154
539D9AFD2507F68400B5765A /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
151-
539D9B012509101E00B5765A /* WebSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
152155
539D9B04250913B300B5765A /* WebSocketConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketConnection.swift; sourceTree = "<group>"; };
153156
736E53F3242A35D90052CC1B /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
154157
736E53F6242A45AC0052CC1B /* XCTest+Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Assertions.swift"; sourceTree = "<group>"; };
@@ -289,7 +292,7 @@
289292
E2B21F06243F5B860049A35B /* PusherEvent.swift */,
290293
3389F5751CAEDE2800563F49 /* PusherGlobalChannel.swift */,
291294
3389F56D1CAEDDD800563F49 /* PusherPresenceChannel.swift */,
292-
539D9B012509101E00B5765A /* WebSocket.swift */,
295+
53140357250B8A9F00F3D7BF /* WebSocket.swift */,
293296
);
294297
path = Models;
295298
sourceTree = "<group>";
@@ -405,6 +408,7 @@
405408
3342F3BB1D808AC500C0296E /* ClientEventTests.swift */,
406409
33BB99611D21226C00B25C2A /* PresenceChannelTests.swift */,
407410
33BB99621D21226C00B25C2A /* PusherChannelTests.swift */,
411+
536F96B7252C841000D2C2F4 /* WebSocketTests.swift */,
408412
);
409413
path = Models;
410414
sourceTree = "<group>";
@@ -765,7 +769,6 @@
765769
3389F5721CAEDDF300563F49 /* PusherChannels.swift in Sources */,
766770
E2CFE43122D79CA7004187C3 /* PusherParser.swift in Sources */,
767771
3389F5761CAEDE2800563F49 /* PusherGlobalChannel.swift in Sources */,
768-
539D9B022509101E00B5765A /* WebSocket.swift in Sources */,
769772
E2B21F01243F5B1E0049A35B /* PusherEventFactory.swift in Sources */,
770773
3389F57A1CAEDEC800563F49 /* PusherClientOptions.swift in Sources */,
771774
E2B21F07243F5B860049A35B /* PusherEvent.swift in Sources */,
@@ -774,6 +777,7 @@
774777
3389F56E1CAEDDD800563F49 /* PusherPresenceChannel.swift in Sources */,
775778
33C1FD6F1D81BFC300921AD7 /* ObjectiveC.swift in Sources */,
776779
3390D1E61F054D0400E1944D /* AuthRequestBuilderProtocol.swift in Sources */,
780+
53140358250B8A9F00F3D7BF /* WebSocket.swift in Sources */,
777781
330D7A6A1CAEDA6C0032FF2C /* PusherSwift.swift in Sources */,
778782
);
779783
runOnlyForDeploymentPostprocessing = 0;
@@ -784,6 +788,7 @@
784788
files = (
785789
73D8A22F2435F393001FDE05 /* PusherEventFactory+DecryptionTests.swift in Sources */,
786790
33BB997A1D21230100B25C2A /* PusherConnectionTests.swift in Sources */,
791+
536F96B8252C841000D2C2F4 /* WebSocketTests.swift in Sources */,
787792
736E53F5242A378B0052CC1B /* String+Extensions.swift in Sources */,
788793
E2F40FA723ED79BC00985C40 /* PusherCryptoTest.swift in Sources */,
789794
33BB99791D21230100B25C2A /* PusherClientInitializationTests.swift in Sources */,
@@ -822,7 +827,6 @@
822827
73D8A1EE2435E5F3001FDE05 /* PusherChannels.swift in Sources */,
823828
73D8A1EF2435E5F3001FDE05 /* PusherParser.swift in Sources */,
824829
539D9AFF2507F69400B5765A /* Constants.swift in Sources */,
825-
539D9B032509101E00B5765A /* WebSocket.swift in Sources */,
826830
73D8A1F02435E5F3001FDE05 /* PusherGlobalChannel.swift in Sources */,
827831
E2B21F02243F5B1E0049A35B /* PusherEventFactory.swift in Sources */,
828832
73D8A1F12435E5F3001FDE05 /* PusherClientOptions.swift in Sources */,
@@ -831,6 +835,7 @@
831835
73D8A1F22435E5F3001FDE05 /* PusherPresenceChannel.swift in Sources */,
832836
73D8A1F32435E5F3001FDE05 /* ObjectiveC.swift in Sources */,
833837
73D8A1F52435E5F3001FDE05 /* AuthRequestBuilderProtocol.swift in Sources */,
838+
53140359250B8A9F00F3D7BF /* WebSocket.swift in Sources */,
834839
73D8A1F62435E5F3001FDE05 /* PusherSwift.swift in Sources */,
835840
);
836841
runOnlyForDeploymentPostprocessing = 0;
@@ -844,6 +849,7 @@
844849
73D8A2102435F24E001FDE05 /* String+Extensions.swift in Sources */,
845850
73D8A2112435F24E001FDE05 /* PusherCryptoTest.swift in Sources */,
846851
D32DBA1B2445BCD60064DA56 /* PrivateEncryptedChannelTests.swift in Sources */,
852+
536F96B9252C841000D2C2F4 /* WebSocketTests.swift in Sources */,
847853
73D8A2122435F24E001FDE05 /* PusherClientInitializationTests.swift in Sources */,
848854
73D8A2132435F24E001FDE05 /* PresenceChannelTests.swift in Sources */,
849855
73D8A2142435F24E001FDE05 /* PusherTopLevelAPITests.swift in Sources */,

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ There are a number of configuration parameters which can be set for the Pusher c
166166

167167
- `authMethod (AuthMethod)` - the method you would like the client to use to authenticate subscription requests to channels requiring authentication (see below for more details)
168168
- `useTLS (Bool)` - whether or not you'd like to use TLS encrypted transport or not, default is `true`
169-
- `autoReconnect (Bool)` - set whether or not you'd like the library to try and autoReconnect upon disconnection
169+
- `autoReconnect (Bool)` - set whether or not you'd like the library to try and automatically reconnect upon disconnection (where possible). See [Reconnection](#reconnection) for more info
170170
- `host (PusherHost)` - set a custom value for the host you'd like to connect to, e.g. `PusherHost.host("ws-test.pusher.com")`
171171
- `port (Int)` - set a custom value for the port that you'd like to connect to
172172
- `activityTimeout (TimeInterval)` - after this time (in seconds) without any messages received from the server, a ping message will be sent to check if the connection is still working; the default value is supplied by the server, low values will result in unnecessary traffic.
@@ -578,6 +578,8 @@ If the Pusher servers close the websocket, or if a disconnection happens due to
578578
579579
All of this is the case if you have the client option of `autoReconnect` set as `true`, which it is by default. If the reconnection strategies are not suitable for your use case then you can set `autoReconnect` to `false` and implement your own reconnection strategy based on the connection state changes.
580580
581+
N.B: If the Pusher servers close the websocket with a [Channels Protocol closure code](https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol#connection-closure), then the `autoReconnect` option is ignored, and the reconnection strategy is determined by the specific closure code that was received.
582+
581583
There are a couple of properties on the connection (`PusherConnection`) that you can set that affect how the reconnection behaviour works. These are:
582584
583585
- `public var reconnectAttemptsMax: Int? = 6` - if you set this to `nil` then there is no maximum number of reconnect attempts and so attempts will continue to be made with an exponential backoff (based on number of attempts), otherwise only as many attempts as this property's value will be made before the connection's state moves to `.disconnected`

Sources/Extensions/PusherWebsocketDelegate.swift

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,15 @@ extension PusherConnection: WebSocketConnectionDelegate {
6868

6969
// Log the disconnection
7070

71-
self.delegate?.debugLog?(message: PusherLogger.debug(for: .disconnectionWithoutError))
71+
logDisconnection(closeCode: closeCode, reason: reason)
7272

7373
// Attempt reconnect if possible
7474

75-
guard self.options.autoReconnect else {
76-
return
75+
// `autoReconnect` option is ignored if the closure code is within the 4000-4999 range
76+
if case .privateCode = closeCode {} else {
77+
guard self.options.autoReconnect else {
78+
return
79+
}
7780
}
7881

7982
guard reconnectAttemptsMax == nil || reconnectAttempts < reconnectAttemptsMax! else {
@@ -85,13 +88,17 @@ extension PusherConnection: WebSocketConnectionDelegate {
8588
self.delegate?.debugLog?(message: PusherLogger.debug(for: .reconnectionFailureLikely))
8689
}
8790

88-
attemptReconnect()
91+
attemptReconnect(closeCode: closeCode)
8992
}
9093

9194
/**
92-
Attempt to reconnect triggered by a disconnection
93-
*/
94-
internal func attemptReconnect() {
95+
Attempt to reconnect triggered by a disconnection.
96+
97+
If the `closeCode` case is `.privateCode()`, then the reconnection logic is determined by
98+
`PusherChannelsProtocolCloseCode.ReconnectionStrategy`.
99+
- Parameter closeCode: The closure code received by the WebSocket connection.
100+
*/
101+
internal func attemptReconnect(closeCode: NWProtocolWebSocket.CloseCode = .protocolCode(.normalClosure)) {
95102
guard connectionState != .connected else {
96103
return
97104
}
@@ -100,27 +107,30 @@ extension PusherConnection: WebSocketConnectionDelegate {
100107
return
101108
}
102109

103-
if connectionState != .reconnecting {
104-
updateConnectionState(to: .reconnecting)
110+
// Reconnect attempt according to Pusher Channels Protocol close code (if present).
111+
// (Otherwise, the default behavior is to attempt reconnection after backing off).
112+
var channelsCloseCode: PusherChannelsProtocolCloseCode?
113+
if case let .privateCode(code) = closeCode {
114+
channelsCloseCode = PusherChannelsProtocolCloseCode(rawValue: code)
105115
}
116+
let strategy = channelsCloseCode?.reconnectionStrategy ?? .reconnectAfterBackingOff
106117

107-
let reconnectInterval = Double(reconnectAttempts * reconnectAttempts)
108-
109-
let timeInterval = maxReconnectGapInSeconds != nil ? min(reconnectInterval, maxReconnectGapInSeconds!)
110-
: reconnectInterval
118+
switch strategy {
119+
case .doNotReconnectUnchanged:
120+
// Return early without attempting reconnection
121+
return
122+
case .reconnectAfterBackingOff,
123+
.reconnectImmediately,
124+
.unknown:
125+
if connectionState != .reconnecting {
126+
updateConnectionState(to: .reconnecting)
127+
}
111128

112-
if reconnectAttemptsMax != nil {
113-
let message = PusherLogger.debug(for: .attemptReconnectionAfterWaiting,
114-
context: "\(timeInterval) seconds (attempt \(reconnectAttempts + 1) of \(reconnectAttemptsMax!))")
115-
self.delegate?.debugLog?(message: message)
116-
} else {
117-
let message = PusherLogger.debug(for: .attemptReconnectionAfterWaiting,
118-
context: "\(timeInterval) seconds (attempt \(reconnectAttempts + 1))")
119-
self.delegate?.debugLog?(message: message)
129+
logReconnectionAttempt(strategy: strategy)
120130
}
121131

122132
reconnectTimer = Timer.scheduledTimer(
123-
timeInterval: timeInterval,
133+
timeInterval: reconnectionAttemptTimeInterval(strategy: strategy),
124134
target: self,
125135
selector: #selector(connect),
126136
userInfo: nil,
@@ -129,6 +139,68 @@ extension PusherConnection: WebSocketConnectionDelegate {
129139
reconnectAttempts += 1
130140
}
131141

142+
/// Returns a `TimeInterval` appropriate for a reconnection attempt after some delay.
143+
/// - Parameter strategy: The reconnection strategy for the reconnection attempt.
144+
/// - Returns: An appropriate `TimeInterval`. (0.0 if `strategy == .reconnectImmediately`).
145+
internal func reconnectionAttemptTimeInterval(strategy: PusherChannelsProtocolCloseCode.ReconnectionStrategy) -> TimeInterval {
146+
if case .reconnectImmediately = strategy {
147+
return 0.0
148+
}
149+
150+
let reconnectInterval = Double(reconnectAttempts * reconnectAttempts)
151+
152+
return maxReconnectGapInSeconds != nil ?
153+
min(reconnectInterval, maxReconnectGapInSeconds!) : reconnectInterval
154+
}
155+
156+
/// Logs the websocket reconnection attempt.
157+
/// - Parameter strategy: The reconnection strategy for the reconnection attempt.
158+
internal func logReconnectionAttempt(strategy: PusherChannelsProtocolCloseCode.ReconnectionStrategy) {
159+
160+
var context = "(attempt \(reconnectAttempts + 1))"
161+
var loggingEvent = PusherLogger.LoggingEvent.attemptReconnectionImmediately
162+
163+
if reconnectAttemptsMax != nil {
164+
context.insert(contentsOf: " of \(reconnectAttemptsMax!)", at: context.index(before: context.endIndex))
165+
}
166+
167+
if strategy != .reconnectImmediately {
168+
loggingEvent = .attemptReconnectionAfterWaiting
169+
let timeInterval = reconnectionAttemptTimeInterval(strategy: strategy)
170+
context = "\(timeInterval) seconds " + context
171+
}
172+
173+
self.delegate?.debugLog?(message: PusherLogger.debug(for: loggingEvent,
174+
context: context))
175+
}
176+
177+
/// Logs the websocket disconnection event.
178+
/// - Parameters:
179+
/// - closeCode: The closure code for the websocket connection.
180+
/// - reason: Optional further information on the connection closure.
181+
internal func logDisconnection(closeCode: NWProtocolWebSocket.CloseCode, reason: Data?) {
182+
var rawCode: UInt16!
183+
switch closeCode {
184+
case .protocolCode(let definedCode):
185+
rawCode = definedCode.rawValue
186+
case .applicationCode(let applicationCode):
187+
rawCode = applicationCode
188+
case .privateCode(let protocolCode):
189+
rawCode = protocolCode
190+
@unknown default:
191+
fatalError()
192+
}
193+
194+
var closeMessage: String = "Close code: \(String(describing: rawCode))."
195+
if let reason = reason,
196+
let reasonString = String(data: reason, encoding: .utf8) {
197+
closeMessage += " Reason: \(reasonString)."
198+
}
199+
200+
self.delegate?.debugLog?(message: PusherLogger.debug(for: .disconnectionWithoutError,
201+
context: closeMessage))
202+
}
203+
132204
/**
133205
Delegate method called when a websocket connected
134206

@@ -144,6 +216,8 @@ extension PusherConnection: WebSocketConnectionDelegate {
144216

145217
func webSocketDidReceiveError(connection: WebSocketConnection, error: Error) {
146218
self.delegate?.debugLog?(message: PusherLogger.debug(for: .errorReceived,
147-
context: "Error (code: \((error as NSError).code)): \(error.localizedDescription)"))
219+
context: """
220+
Error (code: \((error as NSError).code)): \(error.localizedDescription)
221+
"""))
148222
}
149223
}

Sources/Helpers/PusherLogger.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal class PusherLogger {
3131
case attemptReconnectionAfterReachabilityChange =
3232
"Connection state is 'connected' but received network reachability change so going to call attemptReconnect"
3333
case attemptReconnectionAfterWaiting = "Attempting to reconnect after waiting"
34+
case attemptReconnectionImmediately = "Attempting to reconnect immediately"
3435
case connectionEstablished = "Socket established with socket ID:"
3536
case disconnectionWithoutError = "Websocket is disconnected but no error received"
3637
case errorReceived = "Websocket received error."

0 commit comments

Comments
 (0)