Skip to content

Commit 1f00049

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 same collection (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 8f62edc commit 1f00049

File tree

4 files changed

+53
-1
lines changed

4 files changed

+53
-1
lines changed

lib/model/message.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ mixin MessageStore {
1010
/// All known messages, indexed by [Message.id].
1111
Map<int, Message> get messages;
1212

13+
Set<MessageListView> get debugMessageListViews;
14+
1315
void registerMessageList(MessageListView view);
1416
void unregisterMessageList(MessageListView view);
1517

@@ -37,6 +39,9 @@ class MessageStoreImpl with MessageStore {
3739

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

42+
@override
43+
Set<MessageListView> get debugMessageListViews => _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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,9 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
379379
// TODO(#650) notify [recentDmConversationsView] of the just-fetched messages
380380
}
381381

382+
@override
383+
Set<MessageListView> get debugMessageListViews => _messages.debugMessageListViews;
384+
382385
final MessageStoreImpl _messages;
383386

384387
final Unreads unreads;

test/model/message_test.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ void main() {
7777
checkNotified(count: messageList.fetched ? messages.length : 0);
7878
}
7979

80+
test('disposing multiple registered MessageListView instances', () async {
81+
// Regression test for: https://github.com/zulip/zulip-flutter/issues/810
82+
await prepare(narrow: const MentionsNarrow());
83+
MessageListView.init(store: store, narrow: const StarredMessagesNarrow());
84+
check(store.debugMessageListViews).length.equals(2);
85+
86+
// When disposing, the `MessageListView`'s are expected to unregister
87+
// themselves from the message store.
88+
store.dispose();
89+
check(store.debugMessageListViews).isEmpty();
90+
});
91+
8092
group('reconcileMessages', () {
8193
test('from empty', () async {
8294
await prepare();

test/model/store_test.dart

Lines changed: 29 additions & 0 deletions
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,33 @@ 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.debugMessageListViews).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+
454+
// The old store's [MessageListView]s have been disposed.
455+
// (And no exception was thrown; that was #810.)
456+
check(store.debugMessageListViews).isEmpty();
457+
}));
458+
430459
void checkRetry(void Function() prepareError) {
431460
awaitFakeAsync((async) async {
432461
await prepareStore(lastEventId: 1);

0 commit comments

Comments
 (0)