From 952293ac5c9928aedecdb712457ea4aec5750115 Mon Sep 17 00:00:00 2001 From: rostopira Date: Fri, 9 Aug 2019 18:52:30 +0300 Subject: [PATCH] Add LiveQuery support for Flutter Web --- lib/parse_server_sdk.dart | 6 +- lib/src/network/parse_live_query.dart | 13 +- lib/src/network/parse_live_query_web.dart | 182 ++++++++++++++++++++++ lib/src/network/parse_query.dart | 4 +- pubspec.yaml | 2 +- 5 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 lib/src/network/parse_live_query_web.dart diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index 671c7af85..529005f81 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -17,9 +17,11 @@ import 'package:sembast/sembast.dart'; import 'package:sembast/sembast_io.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; -import 'package:web_socket_channel/io.dart'; import 'package:xxtea/xxtea.dart'; +export 'src/network/parse_live_query.dart' + if (dart.library.js) 'src/network/parse_live_query_web.dart'; + part 'package:parse_server_sdk/src/objects/response/parse_error_response.dart'; part 'package:parse_server_sdk/src/objects/response/parse_exception_response.dart'; @@ -46,8 +48,6 @@ part 'src/enums/parse_enum_api_rq.dart'; part 'src/network/parse_http_client.dart'; -part 'src/network/parse_live_query.dart'; - part 'src/network/parse_query.dart'; part 'src/objects/parse_acl.dart'; diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 3ef3785d2..079c62b30 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -1,4 +1,9 @@ -part of flutter_parse_sdk; +import 'dart:convert'; +import 'dart:io'; + +import 'package:web_socket_channel/io.dart'; + +import '../../parse_server_sdk.dart'; enum LiveQueryEvent { create, enter, update, leave, delete, error } @@ -51,7 +56,7 @@ class LiveQuery { final String _className = query.object.parseClassName; final List keysToReturn = query.limiters['keys']?.split(','); query.limiters.clear(); //Remove limits in LiveQuery - final String _where = query._buildQuery().replaceAll('where=', ''); + final String _where = query.buildQuery().replaceAll('where=', ''); //Convert where condition to Map Map _whereMap = Map(); @@ -89,7 +94,7 @@ class LiveQuery { final String className = map['className']; if (className == '_User') { eventCallbacks[actionData['op']]( - ParseUser._getEmptyUser().fromJson(map)); + ParseUser(null, null, null).fromJson(map)); } else { eventCallbacks[actionData['op']]( ParseObject(className).fromJson(map)); @@ -188,4 +193,4 @@ class LiveQuery { await _webSocket.close(); } } -} +} \ No newline at end of file diff --git a/lib/src/network/parse_live_query_web.dart b/lib/src/network/parse_live_query_web.dart new file mode 100644 index 000000000..14d9af0f7 --- /dev/null +++ b/lib/src/network/parse_live_query_web.dart @@ -0,0 +1,182 @@ +import 'dart:convert'; +// ignore: uri_does_not_exist +import 'dart:html'; + +import '../../parse_server_sdk.dart'; + +enum LiveQueryEvent { create, enter, update, leave, delete, error } + +class LiveQuery { + LiveQuery({bool debug, ParseHTTPClient client, bool autoSendSessionId}) { + _client = client ?? + ParseHTTPClient( + sendSessionId: + autoSendSessionId ?? ParseCoreData().autoSendSessionId, + securityContext: ParseCoreData().securityContext); + + _debug = isDebugEnabled(objectLevelDebug: debug); + _sendSessionId = + autoSendSessionId ?? ParseCoreData().autoSendSessionId ?? true; + } + + WebSocket _webSocket; + ParseHTTPClient _client; + bool _debug; + bool _sendSessionId; + Map _connectMessage; + Map _subscribeMessage; + Map _unsubscribeMessage; + Map eventCallbacks = {}; + int _requestIdCount = 1; + final List _liveQueryEvent = [ + 'create', + 'enter', + 'update', + 'leave', + 'delete', + 'error' + ]; + final String _printConstLiveQuery = 'LiveQuery: '; + + int _requestIdGenerator() { + return _requestIdCount++; + } + + // ignore: always_specify_types + Future subscribe(QueryBuilder query) async { + String _liveQueryURL = _client.data.liveQueryURL; + if (_liveQueryURL.contains('https')) { + _liveQueryURL = _liveQueryURL.replaceAll('https', 'wss'); + } else if (_liveQueryURL.contains('http')) { + _liveQueryURL = _liveQueryURL.replaceAll('http', 'ws'); + } + + final String _className = query.object.parseClassName; + final List keysToReturn = query.limiters['keys']?.split(','); + query.limiters.clear(); //Remove limits in LiveQuery + final String _where = query.buildQuery().replaceAll('where=', ''); + + //Convert where condition to Map + Map _whereMap = Map(); + if (_where != '') { + _whereMap = json.decode(_where); + } + + final int requestId = _requestIdGenerator(); + + try { + _webSocket = new WebSocket(_liveQueryURL); + await _webSocket.onOpen.first; + + if (_webSocket != null && _webSocket.readyState == WebSocket.OPEN) { + if (_debug) { + print('$_printConstLiveQuery: Socket opened'); + } + } else { + if (_debug) { + print('$_printConstLiveQuery: Error when connection client'); + return Future.value(null); + } + } + + + _webSocket.onMessage.listen((MessageEvent e) { + final dynamic message = e.data; + if (_debug) { + print('$_printConstLiveQuery: Listen: $message'); + } + + final Map actionData = jsonDecode(message); + + if (eventCallbacks.containsKey(actionData['op'])) { + if (actionData.containsKey('object')) { + final Map map = actionData['object']; + final String className = map['className']; + eventCallbacks[actionData['op']]( + ParseObject(className).fromJson(map)); + } else { + eventCallbacks[actionData['op']](actionData); + } + } + }, onDone: () { + if (_debug) { + print('$_printConstLiveQuery: Done'); + } + }, onError: (Object error) { + if (_debug) { + print( + '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); + } + return Future.value(handleException( + Exception(error), ParseApiRQ.liveQuery, _debug, _className)); + }); + + //The connect message is sent from a client to the LiveQuery server. + //It should be the first message sent from a client after the WebSocket connection is established. + _connectMessage = { + 'op': 'connect', + 'applicationId': _client.data.applicationId + }; + if (_sendSessionId) { + _connectMessage['sessionToken'] = _client.data.sessionId; + } + + if (_client.data.clientKey != null) + _connectMessage['clientKey'] = _client.data.clientKey; + if (_client.data.masterKey != null) + _connectMessage['masterKey'] = _client.data.masterKey; + + if (_debug) { + print('$_printConstLiveQuery: ConnectMessage: $_connectMessage'); + } + _webSocket.sendString(jsonEncode(_connectMessage)); + + //After a client connects to the LiveQuery server, + //it can send a subscribe message to subscribe a ParseQuery. + _subscribeMessage = { + 'op': 'subscribe', + 'requestId': requestId, + 'query': { + 'className': _className, + 'where': _whereMap, + if (keysToReturn != null && keysToReturn.isNotEmpty) + 'fields': keysToReturn + } + }; + if (_sendSessionId) { + _subscribeMessage['sessionToken'] = _client.data.sessionId; + } + + if (_debug) { + print('$_printConstLiveQuery: SubscribeMessage: $_subscribeMessage'); + } + + _webSocket.sendString(jsonEncode(_subscribeMessage)); + + //Mount message for Unsubscribe + _unsubscribeMessage = { + 'op': 'unsubscribe', + 'requestId': requestId, + }; + } on Exception catch (e) { + if (_debug) { + print('$_printConstLiveQuery: Error: ${e.toString()}'); + } + return handleException(e, ParseApiRQ.liveQuery, _debug, _className); + } + } + + void on(LiveQueryEvent op, Function callback) { + eventCallbacks[_liveQueryEvent[op.index]] = callback; + } + + Future unSubscribe() async { + if (_webSocket != null && _webSocket.readyState == WebSocket.OPEN) { + _webSocket.sendString(jsonEncode(_unsubscribeMessage)); + if (_debug) { + print('$_printConstLiveQuery: Socket closed'); + } + await _webSocket.close(); + } + } +} \ No newline at end of file diff --git a/lib/src/network/parse_query.dart b/lib/src/network/parse_query.dart index 2b0de1caa..1f063baf2 100644 --- a/lib/src/network/parse_query.dart +++ b/lib/src/network/parse_query.dart @@ -275,7 +275,7 @@ class QueryBuilder { /// /// Make sure to call this after defining your queries Future query() async { - return object.query(_buildQuery()); + return object.query(buildQuery()); } Future distinct(String className) async { @@ -289,7 +289,7 @@ class QueryBuilder { } /// Builds the query for Parse - String _buildQuery() { + String buildQuery() { queries = _checkForMultipleColumnInstances(queries); return 'where={${buildQueries(queries)}}${getLimiters(limiters)}'; } diff --git a/pubspec.yaml b/pubspec.yaml index b209c4816..84d761252 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/phillwiggins/flutter_parse_sdk author: PhillWiggins environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.2.2 <3.0.0" dependencies: flutter: