|
25 | 25 | private let LIBRARY_NAME_AND_VERSION = "pusher-websocket-swift " + VERSION
|
26 | 26 |
|
27 | 27 | private let URLSession = Foundation.URLSession.shared
|
28 |
| - private var failedNativeServiceRequests: Int = 0 |
| 28 | + private var failedRequestAttempts: Int = 0 |
29 | 29 | private let maxFailedRequestAttempts: Int = 6
|
30 | 30 |
|
| 31 | + internal var socketConnection: PusherConnection? = nil |
| 32 | + |
| 33 | + private var requestQueue = TaskQueue() |
| 34 | + |
31 | 35 | /**
|
32 |
| - Identifies a Pusher app. |
33 |
| - This app should have push notifications enabled. |
| 36 | + Identifies a Pusher app, which should have push notifications enabled |
| 37 | + and a certificate added for the push notifications to work. |
34 | 38 | */
|
35 | 39 | private var pusherAppKey: String? = nil
|
36 | 40 |
|
| 41 | + /** |
| 42 | + The id issued to this app instance by Pusher, which is received upon |
| 43 | + registrations. It's used to identify a client when subscribe / |
| 44 | + unsubscribe requests are made. |
| 45 | + */ |
| 46 | + private var clientId: String? = nil |
| 47 | + |
| 48 | + /** |
| 49 | + Normal clients should access the shared instance via Pusher.nativePusher(). |
| 50 | + */ |
| 51 | + private override init() {} |
| 52 | + |
37 | 53 | /**
|
38 | 54 | Sets the pusherAppKey property and then attempts to flush
|
39 | 55 | the outbox of any pending requests
|
|
42 | 58 | */
|
43 | 59 | open func setPusherAppKey(pusherAppKey: String) {
|
44 | 60 | self.pusherAppKey = pusherAppKey
|
45 |
| - tryFlushOutbox() |
| 61 | + requestQueue.run() |
46 | 62 | }
|
47 | 63 |
|
48 |
| - /** |
49 |
| - The id issued to this app instance by Pusher. |
50 |
| - We get it upon registration. |
51 |
| - We use it to identify ourselves when subscribing/unsubscribing. |
52 |
| - */ |
53 |
| - private var clientId: String? = nil |
54 |
| - |
55 |
| - /** |
56 |
| - Queued actions to perform when the client is registered. |
57 |
| - */ |
58 |
| - private var outbox: [(String, SubscriptionChange)] = [] |
59 |
| - |
60 |
| - /** |
61 |
| - Normal clients should access the shared instance via Pusher.nativePusher(). |
62 |
| - */ |
63 |
| - private override init() {} |
64 | 64 |
|
65 | 65 | /**
|
66 | 66 | Makes device token presentable to server
|
|
101 | 101 | request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
102 | 102 | request.addValue(LIBRARY_NAME_AND_VERSION, forHTTPHeaderField: "X-Pusher-Library" )
|
103 | 103 |
|
104 |
| - |
105 |
| - |
106 | 104 | let task = URLSession.dataTask(with: request, completionHandler: { data, response, error in
|
107 | 105 | if let httpResponse = response as? HTTPURLResponse,
|
108 | 106 | (httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) {
|
|
124 | 122 | if let clientIdJson = json["id"] {
|
125 | 123 | if let clientId = clientIdJson as? String {
|
126 | 124 | self.clientId = clientId
|
127 |
| - self.tryFlushOutbox() |
| 125 | + self.socketConnection?.delegate?.debugLog?(message: "Successfully registered for push notifications and got clientId: \(clientId)") |
| 126 | + self.requestQueue.run() |
128 | 127 | } else {
|
129 |
| - print("Value at \"id\" key in JSON response was not a string: \(json)") |
| 128 | + self.socketConnection?.delegate?.debugLog?(message: "Value at \"id\" key in JSON response was not a string: \(json)") |
130 | 129 | }
|
131 | 130 | } else {
|
132 |
| - print("No \"id\" key in JSON response: \(json)") |
| 131 | + self.socketConnection?.delegate?.debugLog?(message: "No \"id\" key in JSON response: \(json)") |
133 | 132 | }
|
134 | 133 | } else {
|
135 |
| - print("Could not parse body as JSON object: \(data)") |
| 134 | + self.socketConnection?.delegate?.debugLog?(message: "Could not parse body as JSON object: \(data)") |
136 | 135 | }
|
137 | 136 | } else {
|
138 | 137 | if data != nil && response != nil {
|
139 | 138 | let responseBody = String(data: data!, encoding: .utf8)
|
140 |
| - print("Bad HTTP response: \(response!) with body: \(responseBody)") |
| 139 | + self.socketConnection?.delegate?.debugLog?(message: "Bad HTTP response: \(response!) with body: \(responseBody)") |
141 | 140 | }
|
142 | 141 | }
|
143 | 142 | })
|
|
151 | 150 | - parameter interestName: the name of the interest you want to subscribe to
|
152 | 151 | */
|
153 | 152 | open func subscribe(interestName: String) {
|
154 |
| - outbox.append(interestName, SubscriptionChange.subscribe) |
155 |
| - tryFlushOutbox() |
| 153 | + addSubscriptionChangeToTaskQueue(interestName: interestName, change: .subscribe) |
156 | 154 | }
|
157 | 155 |
|
158 | 156 | /**
|
|
162 | 160 | from
|
163 | 161 | */
|
164 | 162 | open func unsubscribe(interestName: String) {
|
165 |
| - outbox.append(interestName, SubscriptionChange.unsubscribe) |
166 |
| - tryFlushOutbox() |
| 163 | + addSubscriptionChangeToTaskQueue(interestName: interestName, change: .unsubscribe) |
167 | 164 | }
|
168 | 165 |
|
169 | 166 | /**
|
170 |
| - Attempts to flush the outbox by making the appropriate requests to either |
171 |
| - subscribe to or unsubscribe from an interest |
| 167 | + Adds subscribe / unsubscribe tasts to task queue |
| 168 | + |
| 169 | + - parameter interestName: the name of the interest you want to interact with |
| 170 | + - parameter change: specifies whether the change is to subscribe or |
| 171 | + unsubscribe |
| 172 | + |
172 | 173 | */
|
173 |
| - private func tryFlushOutbox() { |
174 |
| - switch (self.pusherAppKey, self.clientId) { |
175 |
| - case (.some(let pusherAppKey), .some(let clientId)): |
176 |
| - if (0 < outbox.count) { |
177 |
| - let (interest, change) = outbox.remove(at: 0) |
178 |
| - modifySubscription(pusherAppKey: pusherAppKey, clientId: clientId, interest: interest, change: change) { |
179 |
| - self.tryFlushOutbox() |
180 |
| - } |
181 |
| - } |
182 |
| - case _: break |
| 174 | + private func addSubscriptionChangeToTaskQueue(interestName: String, change: SubscriptionChange) { |
| 175 | + requestQueue.tasks += { _, next in |
| 176 | + self.modifySubscription( |
| 177 | + interest: interestName, |
| 178 | + change: change, |
| 179 | + successCallback: next |
| 180 | + ) |
183 | 181 | }
|
| 182 | + |
| 183 | + requestQueue.run() |
184 | 184 | }
|
185 | 185 |
|
186 | 186 | /**
|
|
193 | 193 | - parameter change: Whether to subscribe or unsubscribe
|
194 | 194 | - parameter callback: Callback to be called upon success
|
195 | 195 | */
|
196 |
| - private func modifySubscription(pusherAppKey: String, clientId: String, interest: String, change: SubscriptionChange, callback: @escaping (Void) -> (Void)) { |
197 |
| - let url = "\(CLIENT_API_V1_ENDPOINT)/clients/\(clientId)/interests/\(interest)" |
198 |
| - var request = URLRequest(url: URL(string: url)!) |
199 |
| - switch (change) { |
200 |
| - case .subscribe: |
201 |
| - request.httpMethod = "POST" |
202 |
| - case .unsubscribe: |
203 |
| - request.httpMethod = "DELETE" |
| 196 | + private func modifySubscription(interest: String, change: SubscriptionChange, successCallback: @escaping (Any?) -> Void) { |
| 197 | + guard pusherAppKey != nil, clientId != nil else { |
| 198 | + self.socketConnection?.delegate?.debugLog?(message: "pusherAppKey \(pusherAppKey) or clientId \(clientId) not set - will retry in 1 second") |
| 199 | + return self.requestQueue.retry(1) |
204 | 200 | }
|
205 | 201 |
|
206 |
| - let params: [String: Any] = ["app_key": pusherAppKey] |
| 202 | + self.socketConnection?.delegate?.debugLog?(message: "Attempt number: \(self.failedRequestAttempts + 1) of \(maxFailedRequestAttempts)") |
| 203 | + |
| 204 | + let url = "\(CLIENT_API_V1_ENDPOINT)/clients/\(clientId!)/interests/\(interest)" |
| 205 | + var request = URLRequest(url: URL(string: url)!) |
| 206 | + request.httpMethod = change.httpMethod() |
207 | 207 |
|
| 208 | + let params: [String: Any] = ["app_key": pusherAppKey!] |
208 | 209 | try! request.httpBody = JSONSerialization.data(withJSONObject: params, options: [])
|
| 210 | + |
209 | 211 | request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
210 | 212 | request.addValue(LIBRARY_NAME_AND_VERSION, forHTTPHeaderField: "X-Pusher-Library")
|
211 | 213 |
|
212 | 214 | let task = URLSession.dataTask(
|
213 | 215 | with: request,
|
214 | 216 | completionHandler: { data, response, error in
|
215 | 217 | guard let httpResponse = response as? HTTPURLResponse,
|
216 |
| - (200 <= httpResponse.statusCode && httpResponse.statusCode < 300) || |
| 218 | + (200 <= httpResponse.statusCode && httpResponse.statusCode < 300) && |
217 | 219 | error == nil
|
218 | 220 | else {
|
219 |
| - self.outbox.insert((interest, change), at: 0) |
| 221 | + self.failedRequestAttempts += 1 |
220 | 222 |
|
221 | 223 | if error != nil {
|
222 |
| - print("Error when trying to modify subscription to interest: \(error?.localizedDescription)") |
| 224 | + self.socketConnection?.delegate?.debugLog?(message: "Error when trying to modify subscription to interest: \(error?.localizedDescription)") |
| 225 | + } else if data != nil && response != nil { |
| 226 | + let responseBody = String(data: data!, encoding: .utf8) |
| 227 | + self.socketConnection?.delegate?.debugLog?(message: "Bad response from server: \(response!) with body: \(responseBody)") |
223 | 228 | } else {
|
224 |
| - print("Bad response from server when trying to modify subscription to interest " + interest) |
| 229 | + self.socketConnection?.delegate?.debugLog?(message: "Bad response from server when trying to modify subscription to interest: \(interest)") |
225 | 230 | }
|
226 |
| - self.failedNativeServiceRequests += 1 |
227 | 231 |
|
228 |
| - if (self.failedNativeServiceRequests < self.maxFailedRequestAttempts) { |
229 |
| - callback() |
| 232 | + if self.failedRequestAttempts > self.maxFailedRequestAttempts { |
| 233 | + self.socketConnection?.delegate?.debugLog?(message: "Max number of failed native service requests reached") |
| 234 | + |
| 235 | + self.requestQueue.paused = true |
230 | 236 | } else {
|
231 |
| - print("Max number of failed native service requests reached") |
| 237 | + self.socketConnection?.delegate?.debugLog?(message: "Retrying subscription modification request for interest: \(interest)") |
| 238 | + self.requestQueue.retry(Double(self.failedRequestAttempts * self.failedRequestAttempts)) |
232 | 239 | }
|
| 240 | + |
233 | 241 | return
|
234 | 242 | }
|
235 | 243 |
|
236 |
| - // Reset number of failed requests to 0 upon success |
237 |
| - self.failedNativeServiceRequests = 0 |
| 244 | + self.socketConnection?.delegate?.debugLog?(message: "Success making \(change.stringValue) to \(interest)") |
238 | 245 |
|
239 |
| - callback() |
| 246 | + self.failedRequestAttempts = 0 |
| 247 | + successCallback(nil) |
240 | 248 | }
|
241 | 249 | )
|
242 | 250 |
|
243 | 251 | task.resume()
|
244 | 252 | }
|
245 | 253 | }
|
246 | 254 |
|
247 |
| -private enum SubscriptionChange { |
| 255 | +internal enum SubscriptionChange { |
248 | 256 | case subscribe
|
249 | 257 | case unsubscribe
|
| 258 | + |
| 259 | + internal func stringValue() -> String { |
| 260 | + switch self { |
| 261 | + case .subscribe: |
| 262 | + return "subscribe" |
| 263 | + case .unsubscribe: |
| 264 | + return "unsubscribe" |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + internal func httpMethod() -> String { |
| 269 | + switch self { |
| 270 | + case .subscribe: |
| 271 | + return "POST" |
| 272 | + case .unsubscribe: |
| 273 | + return "DELETE" |
| 274 | + } |
| 275 | + } |
250 | 276 | }
|
251 | 277 |
|
252 | 278 | #endif
|
0 commit comments