Skip to content

Commit d6948d7

Browse files
authored
LiveList: sync subObjects after reconnect (#352)
* Sync subObjects after reconnect * Update README.md
1 parent 059088e commit d6948d7

2 files changed

Lines changed: 133 additions & 67 deletions

File tree

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,8 +497,6 @@ To activate listening for updates on all included objects, add `listenOnAllSubIt
497497
If you want ParseLiveList to listen for updates on only some sub-objects, use `listeningIncludes: const <String>[/*all the included sub-objects*/]` instead.
498498
Just as QueryBuilder, ParseLiveList supports nested sub-objects too.
499499

500-
**NOTE:** Currently ParseLiveList wont update your sub-objects after your client reconnects to the web.
501-
502500
**NOTE:** To use this features you have to enable [Live Queries](#live-queries) first.
503501

504502
## Users

lib/src/utils/parse_live_list.dart

Lines changed: 133 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -186,48 +186,59 @@ class ParseLiveList<T extends ParseObject> {
186186
.getClientEventStream
187187
.listen((LiveQueryClientEvent event) async {
188188
if (event == LiveQueryClientEvent.CONNECTED) {
189-
final ParseResponse parseResponse = await _runQuery();
190-
if (parseResponse.success) {
191-
final List<T> newList = parseResponse.results ?? List<T>();
192-
193-
//update List
194-
for (int i = 0; i < _list.length; i++) {
195-
final ParseObject currentObject = _list[i].object;
196-
final String currentObjectId =
197-
currentObject.get<String>(keyVarObjectId);
198-
199-
bool stillInList = false;
200-
201-
for (int j = 0; j < newList.length; j++) {
202-
if (newList[j].get<String>(keyVarObjectId) == currentObjectId) {
203-
stillInList = true;
204-
if (newList[j]
205-
.get<DateTime>(keyVarUpdatedAt)
206-
.isAfter(currentObject.get<DateTime>(keyVarUpdatedAt))) {
207-
final QueryBuilder<T> queryBuilder =
208-
QueryBuilder<T>.copy(_query)
209-
..whereEqualTo(keyVarObjectId, currentObjectId);
210-
queryBuilder.query<T>().then((ParseResponse result) {
211-
if (result.success && result.results != null) {
212-
_objectUpdated(result.results.first);
213-
}
214-
});
189+
_updateQueue.whenComplete(() async {
190+
List<Future<void>> tasks = <Future<void>>[];
191+
final ParseResponse parseResponse = await _runQuery();
192+
if (parseResponse.success) {
193+
final List<T> newList = parseResponse.results ?? List<T>();
194+
195+
//update List
196+
for (int i = 0; i < _list.length; i++) {
197+
final ParseObject currentObject = _list[i].object;
198+
final String currentObjectId =
199+
currentObject.get<String>(keyVarObjectId);
200+
201+
bool stillInList = false;
202+
203+
for (int j = 0; j < newList.length; j++) {
204+
if (newList[j].get<String>(keyVarObjectId) == currentObjectId) {
205+
stillInList = true;
206+
if (newList[j]
207+
.get<DateTime>(keyVarUpdatedAt)
208+
.isAfter(currentObject.get<DateTime>(keyVarUpdatedAt))) {
209+
final QueryBuilder<T> queryBuilder =
210+
QueryBuilder<T>.copy(_query)
211+
..whereEqualTo(keyVarObjectId, currentObjectId);
212+
tasks.add(queryBuilder
213+
.query<T>()
214+
.then((ParseResponse result) async {
215+
if (result.success && result.results != null) {
216+
await _objectUpdated(result.results.first);
217+
}
218+
}));
219+
}
220+
newList.removeAt(j);
221+
j--;
222+
break;
215223
}
216-
newList.removeAt(j);
217-
j--;
218-
break;
224+
}
225+
if (!stillInList) {
226+
_objectDeleted(currentObject);
227+
i--;
219228
}
220229
}
221-
if (!stillInList) {
222-
_objectDeleted(currentObject);
223-
i--;
230+
231+
for (int i = 0; i < newList.length; i++) {
232+
tasks.add(_objectAdded(newList[i], loaded: false));
224233
}
225234
}
226-
227-
for (int i = 0; i < newList.length; i++) {
228-
_objectAdded(newList[i], loaded: false);
235+
await Future.wait(tasks);
236+
tasks = <Future<void>>[];
237+
for (ParseLiveListElement<T> element in _list) {
238+
tasks.add(element.reconnected());
229239
}
230-
}
240+
await Future.wait(tasks);
241+
});
231242
}
232243
});
233244
}
@@ -351,7 +362,7 @@ class ParseLiveList<T extends ParseObject> {
351362
_list.removeAt(i).dispose();
352363
_eventStreamController.sink.add(ParseLiveListDeleteEvent<T>(
353364
i, object?.clone(object?.toJson(full: true))));
354-
_objectAdded(object?.clone(object?.toJson(full: true)),
365+
await _objectAdded(object?.clone(object?.toJson(full: true)),
355366
fetchedIncludes: true);
356367
}
357368
break;
@@ -450,7 +461,7 @@ class ParseLiveListElement<T extends ParseObject> {
450461
final StreamController<T> _streamController = StreamController<T>.broadcast();
451462
T _object;
452463
bool _loaded = false;
453-
Map<MapEntry<String, Subscription<ParseObject>>, dynamic> _updatedSubItems;
464+
Map<PathKey, dynamic> _updatedSubItems;
454465
LiveQuery _liveQuery;
455466
final Future<void> _subscriptionQueue = Future<void>.value();
456467

@@ -459,21 +470,17 @@ class ParseLiveListElement<T extends ParseObject> {
459470
// ignore: invalid_use_of_protected_member
460471
T get object => _object?.clone(_object?.toJson(full: true));
461472

462-
Map<MapEntry<String, Subscription<ParseObject>>, dynamic> _toSubscriptionMap(
463-
Map<String, dynamic> map) {
464-
Map<MapEntry<String, Subscription<ParseObject>>, dynamic> result =
465-
Map<MapEntry<String, Subscription<ParseObject>>, dynamic>();
473+
Map<PathKey, dynamic> _toSubscriptionMap(Map<String, dynamic> map) {
474+
final Map<PathKey, dynamic> result = Map<PathKey, dynamic>();
466475
for (String key in map.keys) {
467-
result.putIfAbsent(MapEntry<String, Subscription<ParseObject>>(key, null),
468-
() => _toSubscriptionMap(map[key]));
476+
result.putIfAbsent(PathKey(key), () => _toSubscriptionMap(map[key]));
469477
}
470478
return result;
471479
}
472480

473-
Map<String, dynamic> _toKeyMap(
474-
Map<MapEntry<String, Subscription<ParseObject>>, dynamic> map) {
481+
Map<String, dynamic> _toKeyMap(Map<PathKey, dynamic> map) {
475482
final Map<String, dynamic> result = Map<String, dynamic>();
476-
for (MapEntry<String, Subscription<ParseObject>> key in map.keys) {
483+
for (PathKey key in map.keys) {
477484
result.putIfAbsent(key.key, () => _toKeyMap(map[key]));
478485
}
479486
return result;
@@ -483,8 +490,7 @@ class ParseLiveListElement<T extends ParseObject> {
483490
_subscriptionQueue.whenComplete(() async {
484491
if (_updatedSubItems.isNotEmpty && _object != null) {
485492
final List<Future<void>> tasks = <Future<void>>[];
486-
for (MapEntry<String, Subscription<ParseObject>> key
487-
in _updatedSubItems.keys) {
493+
for (PathKey key in _updatedSubItems.keys) {
488494
tasks.add(_subscribeSubItem(_object, key,
489495
_object.get<ParseObject>(key.key), _updatedSubItems[key]));
490496
}
@@ -493,24 +499,21 @@ class ParseLiveListElement<T extends ParseObject> {
493499
});
494500
}
495501

496-
void _unsubscribe(
497-
Map<MapEntry<String, Subscription<ParseObject>>, dynamic> subscriptions) {
498-
for (MapEntry<String, Subscription<ParseObject>> key
499-
in subscriptions.keys) {
500-
if (_liveQuery != null && key.value != null)
501-
_liveQuery.client.unSubscribe(key.value);
502+
void _unsubscribe(Map<PathKey, dynamic> subscriptions) {
503+
for (PathKey key in subscriptions.keys) {
504+
if (_liveQuery != null && key.subscription != null) {
505+
_liveQuery.client.unSubscribe(key.subscription);
506+
key.subscription = null;
507+
}
502508
_unsubscribe(subscriptions[key]);
503509
}
504510
}
505511

506-
Future<void> _subscribeSubItem(
507-
ParseObject parentObject,
508-
MapEntry<String, Subscription<ParseObject>> currentKey,
509-
ParseObject subObject,
510-
Map<MapEntry<String, Subscription<ParseObject>>, dynamic> path) async {
512+
Future<void> _subscribeSubItem(ParseObject parentObject, PathKey currentKey,
513+
ParseObject subObject, Map<PathKey, dynamic> path) async {
511514
if (_liveQuery != null && subObject != null) {
512515
final List<Future<void>> tasks = <Future<void>>[];
513-
for (MapEntry<String, Subscription<ParseObject>> key in path.keys) {
516+
for (PathKey key in path.keys) {
514517
tasks.add(_subscribeSubItem(
515518
subObject, key, subObject.get<ParseObject>(key.key), path[key]));
516519
}
@@ -521,6 +524,7 @@ class ParseLiveListElement<T extends ParseObject> {
521524
tasks.add(_liveQuery.client
522525
.subscribe(queryBuilder)
523526
.then((Subscription<ParseObject> subscription) {
527+
currentKey.subscription = subscription;
524528
subscription.on(LiveQueryEvent.update, (ParseObject newObject) async {
525529
_subscriptionQueue.whenComplete(() async {
526530
await ParseLiveList._loadIncludes(newObject,
@@ -530,11 +534,11 @@ class ParseLiveListElement<T extends ParseObject> {
530534
_streamController
531535
?.add(_object?.clone(_object?.toJson(full: true)));
532536
//Resubscribe subitems
537+
// TODO(any): only resubscribe on changed pointers
533538
_unsubscribe(path);
534-
for (MapEntry<String, Subscription<ParseObject>> key
535-
in path.keys) {
536-
tasks.add(_subscribeSubItem(subObject, key,
537-
subObject.get<ParseObject>(key.key), path[key]));
539+
for (PathKey key in path.keys) {
540+
tasks.add(_subscribeSubItem(newObject, key,
541+
newObject.get<ParseObject>(key.key), path[key]));
538542
}
539543
}
540544
await Future.wait(tasks);
@@ -560,6 +564,70 @@ class ParseLiveListElement<T extends ParseObject> {
560564
_unsubscribe(_updatedSubItems);
561565
_streamController.close();
562566
}
567+
568+
Future<void> reconnected() async {
569+
if (loaded) {
570+
_subscriptionQueue.whenComplete(() async {
571+
await _updateSubItems(_object, _updatedSubItems);
572+
// _streamController?.add(_object?.clone(_object?.toJson(full: true)));
573+
});
574+
}
575+
}
576+
577+
List<String> _getIncludeList(Map<PathKey, dynamic> path) {
578+
final List<String> includes = <String>[];
579+
for (PathKey key in path.keys) {
580+
includes.add(key.key);
581+
includes.addAll(
582+
_getIncludeList(path[key]).map((String e) => '${key.key}.$e'));
583+
}
584+
return includes;
585+
}
586+
587+
Future<void> _updateSubItems(
588+
ParseObject root, Map<PathKey, dynamic> path) async {
589+
final List<Future<void>> tasks = <Future<void>>[];
590+
for (PathKey key in path.keys) {
591+
ParseObject subObject = root.get<ParseObject>(key.key);
592+
if (subObject?.containsKey(keyVarUpdatedAt) == true) {
593+
final QueryBuilder<ParseObject> queryBuilder =
594+
QueryBuilder<ParseObject>(subObject)
595+
..keysToReturn([keyVarUpdatedAt])
596+
..whereEqualTo(keyVarObjectId, subObject.objectId);
597+
ParseResponse parseResponse = await queryBuilder.query();
598+
if (parseResponse.success &&
599+
(parseResponse.results.first as ParseObject).updatedAt !=
600+
subObject.updatedAt) {
601+
queryBuilder.limiters.remove("keys");
602+
queryBuilder.includeObject(_getIncludeList(path[key]));
603+
ParseResponse parseResponse = await queryBuilder.query();
604+
if (parseResponse.success) {
605+
subObject = parseResponse.result.first;
606+
// root.getObjectData()[key.key] = subObject;
607+
if (key.subscription?.eventCallbacks?.containsKey("update") ==
608+
true) {
609+
key.subscription.eventCallbacks["update"](subObject);
610+
}
611+
// key.subscription.eventCallbacks["update"](subObject);
612+
break;
613+
}
614+
}
615+
}
616+
tasks.add(_updateSubItems(subObject, path[key]));
617+
}
618+
await Future.wait(tasks);
619+
}
620+
}
621+
622+
class PathKey {
623+
PathKey(this.key, {this.subscription});
624+
625+
final String key;
626+
Subscription<ParseObject> subscription;
627+
@override
628+
String toString() {
629+
return 'PathKey(key: $key, subscription: ${subscription?.requestId})';
630+
}
563631
}
564632

565633
abstract class ParseLiveListEvent<T extends ParseObject> {

0 commit comments

Comments
 (0)