Skip to content

Commit 208d5c1

Browse files
PIG208gnprice
authored andcommitted
message: Handle disposal of message store correctly
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: #810 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 6ca5524 commit 208d5c1

File tree

4 files changed

+52
-1
lines changed

4 files changed

+52
-1
lines changed

lib/model/message.dart

+9-1
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

+3
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

+12
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

+28
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,32 @@ 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+
438+
// Make sure there are [MessageListView]s in the message store.
439+
MessageListView.init(store: store, narrow: const MentionsNarrow());
440+
MessageListView.init(store: store, narrow: const StarredMessagesNarrow());
441+
check(store.debugMessageListViews).length.equals(2);
442+
443+
// Let the server expire the event queue.
444+
connection.prepare(httpStatus: 400, json: {
445+
'result': 'error', 'code': 'BAD_EVENT_QUEUE_ID',
446+
'queue_id': updateMachine.queueId,
447+
'msg': 'Bad event queue ID: ${updateMachine.queueId}',
448+
});
449+
updateMachine.debugAdvanceLoop();
450+
async.flushMicrotasks();
451+
await Future<void>.delayed(Duration.zero);
452+
453+
// The old store's [MessageListView]s have been disposed.
454+
// (And no exception was thrown; that was #810.)
455+
check(store.debugMessageListViews).isEmpty();
456+
}));
457+
430458
void checkRetry(void Function() prepareError) {
431459
awaitFakeAsync((async) async {
432460
await prepareStore(lastEventId: 1);

0 commit comments

Comments
 (0)