Skip to content

Commit 2e9b772

Browse files
cbaker6TomWFox
andauthored
Fix LiveQuery reconnection bug (#172)
* WIP * Catch missing errors * Add some publishers for LiveQuery * Make ParseLiveQuery.client public * Don't reuse web socket task after it's been closed * remove asyncs * Fix broken testcases * Check for open socket before sending ping * Add more test cases for livequery * Add changelog entry * nits * Add back playground * Add to subscription list before removing from pending list * Update Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift Co-authored-by: Tom Fox <[email protected]> Co-authored-by: Tom Fox <[email protected]>
1 parent f877b37 commit 2e9b772

File tree

9 files changed

+584
-39
lines changed

9 files changed

+584
-39
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.8.3...main)
55
* _Contributing to this repo? Add info about your change here to be included in the next release_
66

7+
__Fixes__
8+
- Fixed a bug in LiveQuery that prevented reconnecting after a connection was closed. Also added a sendPing method to LiveQuery ([#172](https://github.com/parse-community/Parse-Swift/pull/172)), thanks to [Corey Baker](https://github.com/cbaker6).
9+
710
### 1.8.3
811
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.8.2...1.8.3)
912

ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
import PlaygroundSupport
1111
import Foundation
1212
import ParseSwift
13+
#if canImport(SwiftUI)
1314
import SwiftUI
15+
#if canImport(Combine)
16+
import Combine
17+
#endif
18+
#endif
1419
PlaygroundPage.current.needsIndefiniteExecution = true
1520

1621
initializeParse()
@@ -38,8 +43,9 @@ struct GameScore: ParseObject {
3843
//: Be sure you have LiveQuery enabled on your server.
3944

4045
//: Create a query just as you normally would.
41-
var query = GameScore.query("score" > 9)
46+
var query = GameScore.query("score" < 11)
4247

48+
#if canImport(SwiftUI)
4349
//: To use subscriptions inside of SwiftUI
4450
struct ContentView: View {
4551

@@ -55,7 +61,7 @@ struct ContentView: View {
5561
Text("Unsubscribed from query!")
5662
} else if let event = subscription.event {
5763

58-
//: This is how you register to receive notificaitons of events related to your LiveQuery.
64+
//: This is how you register to receive notifications of events related to your LiveQuery.
5965
switch event.event {
6066

6167
case .entered(let object):
@@ -93,6 +99,7 @@ struct ContentView: View {
9399
}
94100

95101
PlaygroundPage.current.setLiveView(ContentView())
102+
#endif
96103

97104
//: This is how you subscribe to your created query using callbacks.
98105
let subscription = query.subscribeCallback!
@@ -109,7 +116,7 @@ subscription.handleSubscribe { subscribedQuery, isNew in
109116
}
110117
}
111118

112-
//: This is how you register to receive notificaitons of events related to your LiveQuery.
119+
//: This is how you register to receive notifications of events related to your LiveQuery.
113120
subscription.handleEvent { _, event in
114121
switch event {
115122

@@ -126,10 +133,19 @@ subscription.handleEvent { _, event in
126133
}
127134
}
128135

136+
//: Ping the LiveQuery server
137+
ParseLiveQuery.client?.sendPing { error in
138+
if let error = error {
139+
print("Error pinging LiveQuery server: \(error)")
140+
} else {
141+
print("Successfully pinged server!")
142+
}
143+
}
144+
129145
//: Now go to your dashboard, go to the GameScore table and add, update or remove rows.
130146
//: You should receive notifications for each.
131147

132-
//: This is how you register to receive notificaitons about being unsubscribed.
148+
//: This is how you register to receive notifications about being unsubscribed.
133149
subscription.handleUnsubscribe { query in
134150
print("Unsubscribed from \(query)")
135151
}
@@ -141,6 +157,16 @@ do {
141157
print(error)
142158
}
143159

160+
//: Ping the LiveQuery server. This should produce an error
161+
//: because LiveQuery is disconnected.
162+
ParseLiveQuery.client?.sendPing { error in
163+
if let error = error {
164+
print("Error pinging LiveQuery server: \(error)")
165+
} else {
166+
print("Successfully pinged server!")
167+
}
168+
}
169+
144170
//: Create a new query.
145171
var query2 = GameScore.query("score" > 50)
146172

@@ -177,11 +203,86 @@ subscription2.handleEvent { _, event in
177203
}
178204
}
179205

180-
//: Now go to your dashboard, go to the GameScore table and add, update or remove rows.
181-
//: You should receive notifications for each, but only with your fields information.
206+
//: To close the current LiveQuery connection.
207+
ParseLiveQuery.client?.close()
208+
209+
//: To close all LiveQuery connections use:
210+
//ParseLiveQuery.client?.closeAll()
211+
212+
//: Ping the LiveQuery server. This should produce an error
213+
//: because LiveQuery is disconnected.
214+
ParseLiveQuery.client?.sendPing { error in
215+
if let error = error {
216+
print("Error pinging LiveQuery server: \(error)")
217+
} else {
218+
print("Successfully pinged server!")
219+
}
220+
}
221+
222+
//: Subscribe to your new query.
223+
let subscription3 = query2.subscribeCallback!
224+
225+
//: As before, setup your subscription and event handlers.
226+
subscription3.handleSubscribe { subscribedQuery, isNew in
227+
228+
//: You can check this subscription is for this query.
229+
if isNew {
230+
print("Successfully subscribed to new query \(subscribedQuery)")
231+
} else {
232+
print("Successfully updated subscription to new query \(subscribedQuery)")
233+
}
234+
}
235+
236+
subscription3.handleEvent { _, event in
237+
switch event {
238+
239+
case .entered(let object):
240+
print("Entered: \(object)")
241+
case .left(let object):
242+
print("Left: \(object)")
243+
case .created(let object):
244+
print("Created: \(object)")
245+
case .updated(let object):
246+
print("Updated: \(object)")
247+
case .deleted(let object):
248+
print("Deleted: \(object)")
249+
}
250+
}
251+
252+
//: Now lets subscribe to an additional query.
253+
let subscription4 = query.subscribeCallback!
254+
255+
//: This is how you receive notifications about the success
256+
//: of your subscription.
257+
subscription4.handleSubscribe { subscribedQuery, isNew in
258+
259+
//: You can check this subscription is for this query
260+
if isNew {
261+
print("Successfully subscribed to new query \(subscribedQuery)")
262+
} else {
263+
print("Successfully updated subscription to new query \(subscribedQuery)")
264+
}
265+
}
266+
267+
//: This is how you register to receive notifications of events related to your LiveQuery.
268+
subscription4.handleEvent { _, event in
269+
switch event {
270+
271+
case .entered(let object):
272+
print("Entered: \(object)")
273+
case .left(let object):
274+
print("Left: \(object)")
275+
case .created(let object):
276+
print("Created: \(object)")
277+
case .updated(let object):
278+
print("Updated: \(object)")
279+
case .deleted(let object):
280+
print("Deleted: \(object)")
281+
}
282+
}
182283

183-
//: This is how you register to receive notificaitons about being unsubscribed.
184-
subscription2.handleUnsubscribe { query in
284+
//: Now we will will unsubscribe from one of the subsriptions, but maintain the connection.
285+
subscription3.handleUnsubscribe { query in
185286
print("Unsubscribed from \(query)")
186287
}
187288

@@ -192,5 +293,14 @@ do {
192293
print(error)
193294
}
194295

296+
//: Ping the LiveQuery server
297+
ParseLiveQuery.client?.sendPing { error in
298+
if let error = error {
299+
print("Error pinging LiveQuery server: \(error)")
300+
} else {
301+
print("Successfully pinged server!")
302+
}
303+
}
304+
195305
PlaygroundPage.current.finishExecution()
196306
//: [Next](@next)

ParseSwift.xcodeproj/project.pbxproj

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,13 @@
384384
91678706259BC5D400BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; };
385385
91678710259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; };
386386
9167871A259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; };
387+
918CED592684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; };
388+
918CED5A2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; };
389+
918CED5B2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; };
390+
918CED5C2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; };
391+
918CED5E268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */; };
392+
918CED5F268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */; };
393+
918CED60268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */; };
387394
9194657824F16E330070296B /* ParseACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ParseACLTests.swift */; };
388395
91B40651267A66ED00B129CD /* ParseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B40650267A66ED00B129CD /* ParseErrorTests.swift */; };
389396
91B40652267A66ED00B129CD /* ParseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B40650267A66ED00B129CD /* ParseErrorTests.swift */; };
@@ -707,6 +714,8 @@
707714
9158916A256A07DD0024BE9A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
708715
916786E1259B7DDA00BB5B4E /* ParseCloud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloud.swift; sourceTree = "<group>"; };
709716
916786EF259BC59600BB5B4E /* ParseCloudTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudTests.swift; sourceTree = "<group>"; };
717+
918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseLiveQuery+combine.swift"; sourceTree = "<group>"; };
718+
918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryCombineTests.swift; sourceTree = "<group>"; };
710719
9194657724F16E330070296B /* ParseACLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseACLTests.swift; sourceTree = "<group>"; };
711720
91B40650267A66ED00B129CD /* ParseErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseErrorTests.swift; sourceTree = "<group>"; };
712721
91CB9536265966DF0043E5D6 /* ParseAnanlyticsCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnanlyticsCombineTests.swift; sourceTree = "<group>"; };
@@ -888,6 +897,7 @@
888897
70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */,
889898
70386A5B25D9A4010048EC1B /* ParseLDAPCombineTests.swift */,
890899
70386A4525D99C8B0048EC1B /* ParseLDAPTests.swift */,
900+
918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */,
891901
7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */,
892902
70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */,
893903
7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */,
@@ -1044,10 +1054,11 @@
10441054
isa = PBXGroup;
10451055
children = (
10461056
70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */,
1047-
70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */,
10481057
7003959425A10DFC0052CB31 /* Messages.swift */,
10491058
700395A225A119430052CB31 /* Operations.swift */,
10501059
7003960825A184EF0052CB31 /* ParseLiveQuery.swift */,
1060+
918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */,
1061+
70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */,
10511062
700395B925A1470F0052CB31 /* Subscription.swift */,
10521063
705D950725BE4C08003EF6F8 /* SubscriptionCallback.swift */,
10531064
700395DE25A147C40052CB31 /* Protocols */,
@@ -1651,6 +1662,7 @@
16511662
7044C1C825C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */,
16521663
707A3BF125B0A4F0000D215C /* ParseAuthentication.swift in Sources */,
16531664
70D1BE7325BB43EB00A42E7C /* BaseConfig.swift in Sources */,
1665+
918CED592684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */,
16541666
70386A0625D9718C0048EC1B /* Data+hexString.swift in Sources */,
16551667
F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */,
16561668
70170A442656B02D0070C905 /* ParseAnalytics.swift in Sources */,
@@ -1735,6 +1747,7 @@
17351747
isa = PBXSourcesBuildPhase;
17361748
buildActionMask = 2147483647;
17371749
files = (
1750+
918CED5E268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */,
17381751
911DB13624C4FC100027F3C7 /* ParseObjectTests.swift in Sources */,
17391752
70E09E1C262F0634002DD451 /* ParsePointerCombineTests.swift in Sources */,
17401753
89899D592603CF3E002E2043 /* ParseTwitterTests.swift in Sources */,
@@ -1805,6 +1818,7 @@
18051818
7044C1C925C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */,
18061819
707A3BF225B0A4F0000D215C /* ParseAuthentication.swift in Sources */,
18071820
70D1BE7425BB43EB00A42E7C /* BaseConfig.swift in Sources */,
1821+
918CED5A2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */,
18081822
70386A0725D9718C0048EC1B /* Data+hexString.swift in Sources */,
18091823
F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */,
18101824
70170A452656B02D0070C905 /* ParseAnalytics.swift in Sources */,
@@ -1898,6 +1912,7 @@
18981912
isa = PBXSourcesBuildPhase;
18991913
buildActionMask = 2147483647;
19001914
files = (
1915+
918CED60268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */,
19011916
709B98512556ECAA00507778 /* ParseEncoderExtraTests.swift in Sources */,
19021917
70E09E1E262F0634002DD451 /* ParsePointerCombineTests.swift in Sources */,
19031918
89899D642603CF3F002E2043 /* ParseTwitterTests.swift in Sources */,
@@ -1960,6 +1975,7 @@
19601975
isa = PBXSourcesBuildPhase;
19611976
buildActionMask = 2147483647;
19621977
files = (
1978+
918CED5F268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */,
19631979
70F2E2B6254F283000B2EA5C /* ParseACLTests.swift in Sources */,
19641980
70E09E1D262F0634002DD451 /* ParsePointerCombineTests.swift in Sources */,
19651981
89899D632603CF3E002E2043 /* ParseTwitterTests.swift in Sources */,
@@ -2030,6 +2046,7 @@
20302046
7044C1CB25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */,
20312047
707A3BF425B0A4F0000D215C /* ParseAuthentication.swift in Sources */,
20322048
70D1BE7625BB43EB00A42E7C /* BaseConfig.swift in Sources */,
2049+
918CED5C2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */,
20332050
70386A0925D9718C0048EC1B /* Data+hexString.swift in Sources */,
20342051
F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */,
20352052
70170A472656B02D0070C905 /* ParseAnalytics.swift in Sources */,
@@ -2122,6 +2139,7 @@
21222139
7044C1CA25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */,
21232140
707A3BF325B0A4F0000D215C /* ParseAuthentication.swift in Sources */,
21242141
70D1BE7525BB43EB00A42E7C /* BaseConfig.swift in Sources */,
2142+
918CED5B2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */,
21252143
70386A0825D9718C0048EC1B /* Data+hexString.swift in Sources */,
21262144
F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */,
21272145
70170A462656B02D0070C905 /* ParseAnalytics.swift in Sources */,

Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ extension LiveQuerySocket {
5353
.encode(StandardMessage(operation: .connect,
5454
additionalProperties: true))
5555
guard let encodedAsString = String(data: encoded, encoding: .utf8) else {
56+
let error = ParseError(code: .unknownError,
57+
message: "Couldn't encode connect message: \(encoded)")
58+
completion(error)
5659
return
5760
}
5861
task.send(.string(encodedAsString)) { error in
@@ -104,6 +107,15 @@ extension LiveQuerySocket {
104107
}
105108
}
106109

110+
// MARK: Ping
111+
@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *)
112+
extension LiveQuerySocket {
113+
114+
func sendPing(_ task: URLSessionWebSocketTask, pongReceiveHandler: @escaping (Error?) -> Void) {
115+
task.sendPing(pongReceiveHandler: pongReceiveHandler)
116+
}
117+
}
118+
107119
// MARK: URLSession
108120
@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *)
109121
extension URLSession {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// ParseLiveQuery+combine.swift
3+
// ParseSwift
4+
//
5+
// Created by Corey Baker on 6/24/21.
6+
// Copyright © 2021 Parse Community. All rights reserved.
7+
//
8+
9+
#if canImport(Combine)
10+
import Foundation
11+
import Combine
12+
13+
@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *)
14+
extension ParseLiveQuery {
15+
// MARK: Functions - Combine
16+
17+
/**
18+
Manually establish a connection to the `ParseLiveQuery` Server. Publishes when established.
19+
- parameter isUserWantsToConnect: Specifies if the user is calling this function. Defaults to `true`.
20+
- returns: A publisher that eventually produces a single value and then finishes or fails.
21+
*/
22+
public func openPublisher(isUserWantsToConnect: Bool = true) -> Future<Void, Error> {
23+
Future { promise in
24+
self.open(isUserWantsToConnect: isUserWantsToConnect) { error in
25+
guard let error = error else {
26+
promise(.success(()))
27+
return
28+
}
29+
promise(.failure(error))
30+
}
31+
}
32+
}
33+
34+
/**
35+
Sends a ping frame from the client side. Publishes when a pong is received from the
36+
server endpoint.
37+
- returns: A publisher that eventually produces a single value and then finishes or fails.
38+
*/
39+
public func sendPingPublisher() -> Future<Void, Error> {
40+
Future { promise in
41+
self.sendPing { error in
42+
guard let error = error else {
43+
promise(.success(()))
44+
return
45+
}
46+
promise(.failure(error))
47+
}
48+
}
49+
}
50+
}
51+
#endif

0 commit comments

Comments
 (0)