Skip to content

Commit dd47ba8

Browse files
committed
message: Handle disposal of message store correctly
This fixes a bug caused by MessageListView instances unregistering themselves from the collection containing these instances, while we are iterating the exact same set when disposing the message store. Part of the work of disposing a MessageListView is to remove it from a collection of MessageListViews, and we've been doing this in a loop over the exact same collection (the set MessageStoreImpl._messageListViews). This has been causing concurrent modification errors. The errors originated from this requirement of dart Iterable: > Changing a collection while it is being iterated is generally not allowed This fix avoids those errors by having the loop iterate over a clone of that collection instead. See also: https://api.flutter.dev/flutter/dart-core/Iterable-class.html Fixes: zulip#810 Signed-off-by: Zixuan James Li <[email protected]>
1 parent eb1c818 commit dd47ba8

File tree

4 files changed

+63
-1
lines changed

4 files changed

+63
-1
lines changed

lib/model/message.dart

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'dart:convert';
22

3+
import 'package:flutter/foundation.dart';
4+
35
import '../api/model/events.dart';
46
import '../api/model/model.dart';
57
import '../log.dart';
@@ -37,6 +39,9 @@ class MessageStoreImpl with MessageStore {
3739

3840
final Set<MessageListView> _messageListViews = {};
3941

42+
@visibleForTesting
43+
Set<MessageListView> get debugMessageLists => _messageListViews;
44+
4045
@override
4146
void registerMessageList(MessageListView view) {
4247
final added = _messageListViews.add(view);
@@ -56,7 +61,10 @@ class MessageStoreImpl with MessageStore {
5661
}
5762

5863
void dispose() {
59-
for (final view in _messageListViews) {
64+
// When a MessageListView is disposed, it removes itself from the set
65+
// `MessageStoreImpl._messageListViews`, Instead of iterating on that Set,
66+
// iterate on a copy, to avoid concurrent modifications.
67+
for (final view in _messageListViews.toList()) {
6068
view.dispose();
6169
}
6270
}

lib/model/store.dart

+3
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,9 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
381381

382382
final MessageStoreImpl _messages;
383383

384+
@visibleForTesting
385+
MessageStoreImpl get debugMessageStore => _messages;
386+
384387
final Unreads unreads;
385388

386389
final RecentDmConversationsView recentDmConversationsView;

test/model/message_test.dart

+19
Original file line numberDiff line numberDiff line change
@@ -919,4 +919,23 @@ void main() {
919919
});
920920
});
921921
});
922+
923+
test('disposing multiple registered MessageListView instances', () async {
924+
// Regression test for: https://github.com/zulip/zulip-flutter/issues/810
925+
await prepare(narrow: const MentionsNarrow());
926+
MessageListView.init(store: store, narrow: const StarredMessagesNarrow());
927+
928+
Condition<Object?> conditionMessageListView(Narrow narrow) =>
929+
(it) => it.isA<MessageListView>().narrow.equals(narrow);
930+
931+
check(store.debugMessageStore.debugMessageLists).deepEquals([
932+
conditionMessageListView(const MentionsNarrow()),
933+
conditionMessageListView(const StarredMessagesNarrow()),
934+
]);
935+
936+
// When disposing, the `MessageListView`'s are expected to unregister
937+
// themselves from the message store.
938+
store.dispose();
939+
check(store.debugMessageStore.debugMessageLists).isEmpty();
940+
});
922941
}

test/model/store_test.dart

+32
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:zulip/api/model/events.dart';
88
import 'package:zulip/api/model/model.dart';
99
import 'package:zulip/api/route/events.dart';
1010
import 'package:zulip/api/route/messages.dart';
11+
import 'package:zulip/model/message_list.dart';
12+
import 'package:zulip/model/narrow.dart';
1113
import 'package:zulip/model/store.dart';
1214
import 'package:zulip/notifications/receive.dart';
1315

@@ -427,6 +429,36 @@ void main() {
427429
check(store.userSettings!.twentyFourHourTime).isTrue();
428430
}));
429431

432+
test('expired queue disposes registered MessageListView instances', () => awaitFakeAsync((async) async {
433+
// Regression test for: https://github.com/zulip/zulip-flutter/issues/810
434+
await prepareStore();
435+
updateMachine.debugPauseLoop();
436+
updateMachine.poll();
437+
check(globalStore.perAccountSync(store.accountId)).identicalTo(store);
438+
439+
// Make sure there are message list views in the message store.
440+
MessageListView.init(store: store, narrow: const MentionsNarrow());
441+
MessageListView.init(store: store, narrow: const StarredMessagesNarrow());
442+
check(store.debugMessageStore.debugMessageLists).length.equals(2);
443+
444+
// Let the server expire the event queue.
445+
connection.prepare(httpStatus: 400, json: {
446+
'result': 'error', 'code': 'BAD_EVENT_QUEUE_ID',
447+
'queue_id': updateMachine.queueId,
448+
'msg': 'Bad event queue ID: ${updateMachine.queueId}',
449+
});
450+
updateMachine.debugAdvanceLoop();
451+
async.flushMicrotasks();
452+
await Future<void>.delayed(Duration.zero);
453+
check(store).isLoading.isTrue();
454+
check(store.debugMessageStore.debugMessageLists).isEmpty();
455+
456+
// The global store has a new store.
457+
check(globalStore.perAccountSync(store.accountId)).not((it) => it.identicalTo(store));
458+
updateFromGlobalStore();
459+
check(store).isLoading.isFalse();
460+
}));
461+
430462
void checkRetry(void Function() prepareError) {
431463
awaitFakeAsync((async) async {
432464
await prepareStore(lastEventId: 1);

0 commit comments

Comments
 (0)