Skip to content

Commit f0579df

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 eb1c818 commit f0579df

File tree

4 files changed

+57
-1
lines changed

4 files changed

+57
-1
lines changed

lib/model/message.dart

Lines changed: 12 additions & 1 deletion
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';
@@ -10,6 +12,9 @@ mixin MessageStore {
1012
/// All known messages, indexed by [Message.id].
1113
Map<int, Message> get messages;
1214

15+
@visibleForTesting
16+
Set<MessageListView> get debugMessageListsViews;
17+
1318
void registerMessageList(MessageListView view);
1419
void unregisterMessageList(MessageListView view);
1520

@@ -37,6 +42,9 @@ class MessageStoreImpl with MessageStore {
3742

3843
final Set<MessageListView> _messageListViews = {};
3944

45+
@override
46+
Set<MessageListView> get debugMessageListsViews => _messageListViews;
47+
4048
@override
4149
void registerMessageList(MessageListView view) {
4250
final added = _messageListViews.add(view);
@@ -56,7 +64,10 @@ class MessageStoreImpl with MessageStore {
5664
}
5765

5866
void dispose() {
59-
for (final view in _messageListViews) {
67+
// When a MessageListView is disposed, it removes itself from the set
68+
// `MessageStoreImpl._messageListViews`. Instead of iterating on that set,
69+
// iterate on a copy, to avoid concurrent modifications.
70+
for (final view in _messageListViews.toList()) {
6071
view.dispose();
6172
}
6273
}

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 debugMessageListsViews => _messages.debugMessageListsViews;
384+
382385
final MessageStoreImpl _messages;
383386

384387
final Unreads unreads;

test/model/message_test.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@ 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+
85+
check(store.debugMessageListsViews).length.equals(2);
86+
87+
// When disposing, the `MessageListView`'s are expected to unregister
88+
// themselves from the message store.
89+
store.dispose();
90+
check(store.debugMessageListsViews).isEmpty();
91+
});
92+
8093
group('reconcileMessages', () {
8194
test('from empty', () async {
8295
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.debugMessageListsViews).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.debugMessageListsViews).isEmpty();
457+
}));
458+
430459
void checkRetry(void Function() prepareError) {
431460
awaitFakeAsync((async) async {
432461
await prepareStore(lastEventId: 1);

0 commit comments

Comments
 (0)