Skip to content

Commit e49b521

Browse files
rostopiraphillwiggins
authored andcommitted
Add LiveQuery support for Flutter Web (#246)
1 parent ef44ffb commit e49b521

File tree

5 files changed

+197
-10
lines changed

5 files changed

+197
-10
lines changed

lib/parse_server_sdk.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import 'package:sembast/sembast.dart';
1717
import 'package:sembast/sembast_io.dart';
1818
import 'package:shared_preferences/shared_preferences.dart';
1919
import 'package:uuid/uuid.dart';
20-
import 'package:web_socket_channel/io.dart';
2120
import 'package:xxtea/xxtea.dart';
2221

22+
export 'src/network/parse_live_query.dart'
23+
if (dart.library.js) 'src/network/parse_live_query_web.dart';
24+
2325
part 'package:parse_server_sdk/src/objects/response/parse_error_response.dart';
2426

2527
part 'package:parse_server_sdk/src/objects/response/parse_exception_response.dart';
@@ -46,8 +48,6 @@ part 'src/enums/parse_enum_api_rq.dart';
4648

4749
part 'src/network/parse_http_client.dart';
4850

49-
part 'src/network/parse_live_query.dart';
50-
5151
part 'src/network/parse_query.dart';
5252

5353
part 'src/objects/parse_acl.dart';

lib/src/network/parse_live_query.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
part of flutter_parse_sdk;
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:web_socket_channel/io.dart';
5+
6+
import '../../parse_server_sdk.dart';
27

38
enum LiveQueryEvent { create, enter, update, leave, delete, error }
49

@@ -51,7 +56,7 @@ class LiveQuery {
5156
final String _className = query.object.parseClassName;
5257
final List<String> keysToReturn = query.limiters['keys']?.split(',');
5358
query.limiters.clear(); //Remove limits in LiveQuery
54-
final String _where = query._buildQuery().replaceAll('where=', '');
59+
final String _where = query.buildQuery().replaceAll('where=', '');
5560

5661
//Convert where condition to Map
5762
Map<String, dynamic> _whereMap = Map<String, dynamic>();
@@ -89,7 +94,7 @@ class LiveQuery {
8994
final String className = map['className'];
9095
if (className == '_User') {
9196
eventCallbacks[actionData['op']](
92-
ParseUser._getEmptyUser().fromJson(map));
97+
ParseUser(null, null, null).fromJson(map));
9398
} else {
9499
eventCallbacks[actionData['op']](
95100
ParseObject(className).fromJson(map));
@@ -188,4 +193,4 @@ class LiveQuery {
188193
await _webSocket.close();
189194
}
190195
}
191-
}
196+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import 'dart:convert';
2+
// ignore: uri_does_not_exist
3+
import 'dart:html';
4+
5+
import '../../parse_server_sdk.dart';
6+
7+
enum LiveQueryEvent { create, enter, update, leave, delete, error }
8+
9+
class LiveQuery {
10+
LiveQuery({bool debug, ParseHTTPClient client, bool autoSendSessionId}) {
11+
_client = client ??
12+
ParseHTTPClient(
13+
sendSessionId:
14+
autoSendSessionId ?? ParseCoreData().autoSendSessionId,
15+
securityContext: ParseCoreData().securityContext);
16+
17+
_debug = isDebugEnabled(objectLevelDebug: debug);
18+
_sendSessionId =
19+
autoSendSessionId ?? ParseCoreData().autoSendSessionId ?? true;
20+
}
21+
22+
WebSocket _webSocket;
23+
ParseHTTPClient _client;
24+
bool _debug;
25+
bool _sendSessionId;
26+
Map<String, dynamic> _connectMessage;
27+
Map<String, dynamic> _subscribeMessage;
28+
Map<String, dynamic> _unsubscribeMessage;
29+
Map<String, Function> eventCallbacks = <String, Function>{};
30+
int _requestIdCount = 1;
31+
final List<String> _liveQueryEvent = <String>[
32+
'create',
33+
'enter',
34+
'update',
35+
'leave',
36+
'delete',
37+
'error'
38+
];
39+
final String _printConstLiveQuery = 'LiveQuery: ';
40+
41+
int _requestIdGenerator() {
42+
return _requestIdCount++;
43+
}
44+
45+
// ignore: always_specify_types
46+
Future subscribe(QueryBuilder query) async {
47+
String _liveQueryURL = _client.data.liveQueryURL;
48+
if (_liveQueryURL.contains('https')) {
49+
_liveQueryURL = _liveQueryURL.replaceAll('https', 'wss');
50+
} else if (_liveQueryURL.contains('http')) {
51+
_liveQueryURL = _liveQueryURL.replaceAll('http', 'ws');
52+
}
53+
54+
final String _className = query.object.parseClassName;
55+
final List<String> keysToReturn = query.limiters['keys']?.split(',');
56+
query.limiters.clear(); //Remove limits in LiveQuery
57+
final String _where = query.buildQuery().replaceAll('where=', '');
58+
59+
//Convert where condition to Map
60+
Map<String, dynamic> _whereMap = Map<String, dynamic>();
61+
if (_where != '') {
62+
_whereMap = json.decode(_where);
63+
}
64+
65+
final int requestId = _requestIdGenerator();
66+
67+
try {
68+
_webSocket = new WebSocket(_liveQueryURL);
69+
await _webSocket.onOpen.first;
70+
71+
if (_webSocket != null && _webSocket.readyState == WebSocket.OPEN) {
72+
if (_debug) {
73+
print('$_printConstLiveQuery: Socket opened');
74+
}
75+
} else {
76+
if (_debug) {
77+
print('$_printConstLiveQuery: Error when connection client');
78+
return Future<void>.value(null);
79+
}
80+
}
81+
82+
83+
_webSocket.onMessage.listen((MessageEvent e) {
84+
final dynamic message = e.data;
85+
if (_debug) {
86+
print('$_printConstLiveQuery: Listen: $message');
87+
}
88+
89+
final Map<String, dynamic> actionData = jsonDecode(message);
90+
91+
if (eventCallbacks.containsKey(actionData['op'])) {
92+
if (actionData.containsKey('object')) {
93+
final Map<String, dynamic> map = actionData['object'];
94+
final String className = map['className'];
95+
eventCallbacks[actionData['op']](
96+
ParseObject(className).fromJson(map));
97+
} else {
98+
eventCallbacks[actionData['op']](actionData);
99+
}
100+
}
101+
}, onDone: () {
102+
if (_debug) {
103+
print('$_printConstLiveQuery: Done');
104+
}
105+
}, onError: (Object error) {
106+
if (_debug) {
107+
print(
108+
'$_printConstLiveQuery: Error: ${error.runtimeType.toString()}');
109+
}
110+
return Future<ParseResponse>.value(handleException(
111+
Exception(error), ParseApiRQ.liveQuery, _debug, _className));
112+
});
113+
114+
//The connect message is sent from a client to the LiveQuery server.
115+
//It should be the first message sent from a client after the WebSocket connection is established.
116+
_connectMessage = <String, String>{
117+
'op': 'connect',
118+
'applicationId': _client.data.applicationId
119+
};
120+
if (_sendSessionId) {
121+
_connectMessage['sessionToken'] = _client.data.sessionId;
122+
}
123+
124+
if (_client.data.clientKey != null)
125+
_connectMessage['clientKey'] = _client.data.clientKey;
126+
if (_client.data.masterKey != null)
127+
_connectMessage['masterKey'] = _client.data.masterKey;
128+
129+
if (_debug) {
130+
print('$_printConstLiveQuery: ConnectMessage: $_connectMessage');
131+
}
132+
_webSocket.sendString(jsonEncode(_connectMessage));
133+
134+
//After a client connects to the LiveQuery server,
135+
//it can send a subscribe message to subscribe a ParseQuery.
136+
_subscribeMessage = <String, dynamic>{
137+
'op': 'subscribe',
138+
'requestId': requestId,
139+
'query': <String, dynamic>{
140+
'className': _className,
141+
'where': _whereMap,
142+
if (keysToReturn != null && keysToReturn.isNotEmpty)
143+
'fields': keysToReturn
144+
}
145+
};
146+
if (_sendSessionId) {
147+
_subscribeMessage['sessionToken'] = _client.data.sessionId;
148+
}
149+
150+
if (_debug) {
151+
print('$_printConstLiveQuery: SubscribeMessage: $_subscribeMessage');
152+
}
153+
154+
_webSocket.sendString(jsonEncode(_subscribeMessage));
155+
156+
//Mount message for Unsubscribe
157+
_unsubscribeMessage = <String, dynamic>{
158+
'op': 'unsubscribe',
159+
'requestId': requestId,
160+
};
161+
} on Exception catch (e) {
162+
if (_debug) {
163+
print('$_printConstLiveQuery: Error: ${e.toString()}');
164+
}
165+
return handleException(e, ParseApiRQ.liveQuery, _debug, _className);
166+
}
167+
}
168+
169+
void on(LiveQueryEvent op, Function callback) {
170+
eventCallbacks[_liveQueryEvent[op.index]] = callback;
171+
}
172+
173+
Future<void> unSubscribe() async {
174+
if (_webSocket != null && _webSocket.readyState == WebSocket.OPEN) {
175+
_webSocket.sendString(jsonEncode(_unsubscribeMessage));
176+
if (_debug) {
177+
print('$_printConstLiveQuery: Socket closed');
178+
}
179+
await _webSocket.close();
180+
}
181+
}
182+
}

lib/src/network/parse_query.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ class QueryBuilder<T extends ParseObject> {
275275
///
276276
/// Make sure to call this after defining your queries
277277
Future<ParseResponse> query() async {
278-
return object.query(_buildQuery());
278+
return object.query(buildQuery());
279279
}
280280

281281
Future<ParseResponse> distinct(String className) async {
@@ -289,7 +289,7 @@ class QueryBuilder<T extends ParseObject> {
289289
}
290290

291291
/// Builds the query for Parse
292-
String _buildQuery() {
292+
String buildQuery() {
293293
queries = _checkForMultipleColumnInstances(queries);
294294
return 'where={${buildQueries(queries)}}${getLimiters(limiters)}';
295295
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ homepage: https://github.com/phillwiggins/flutter_parse_sdk
55
author: PhillWiggins <phill.wiggins@gmail.com>
66

77
environment:
8-
sdk: ">=2.0.0-dev.68.0 <3.0.0"
8+
sdk: ">=2.2.2 <3.0.0"
99

1010
dependencies:
1111
flutter:

0 commit comments

Comments
 (0)