Skip to content

Commit a11d46a

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. The error originated from this requirement of dart Iterable: > Changing a collection while it is being iterated is generally not allowed See also: https://api.flutter.dev/flutter/dart-core/Iterable-class.html Fixes: #810 Signed-off-by: Zixuan James Li <[email protected]>
1 parent edc9a40 commit a11d46a

File tree

4 files changed

+59
-0
lines changed

4 files changed

+59
-0
lines changed

lib/model/message.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:flutter/foundation.dart';
2+
13
import '../api/model/events.dart';
24
import '../api/model/model.dart';
35
import '../log.dart';
@@ -35,6 +37,9 @@ class MessageStoreImpl with MessageStore {
3537

3638
final Set<MessageListView> _messageListViews = {};
3739

40+
@visibleForTesting
41+
Set<MessageListView> get debugMessageLists => _messageListViews;
42+
3843
@override
3944
void registerMessageList(MessageListView view) {
4045
final added = _messageListViews.add(view);

lib/model/store.dart

Lines changed: 3 additions & 0 deletions
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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,4 +576,23 @@ void main() {
576576
.reactions.isNotNull().jsonEquals([eg.unicodeEmojiReaction]);
577577
});
578578
});
579+
580+
test('disposing multiple registered MessageListView instances', () async {
581+
// Regression test for: https://github.com/zulip/zulip-flutter/issues/810
582+
await prepare(narrow: const MentionsNarrow());
583+
MessageListView.init(store: store, narrow: const StarredMessagesNarrow());
584+
585+
Condition<Object?> conditionMessageListView(Narrow narrow) =>
586+
(it) => it.isA<MessageListView>().narrow.equals(narrow);
587+
588+
check(store.debugMessageStore.debugMessageLists).deepEquals([
589+
conditionMessageListView(const MentionsNarrow()),
590+
conditionMessageListView(const StarredMessagesNarrow()),
591+
]);
592+
593+
// When disposing, the `MessageListView`'s are expected to unregister
594+
// themselves from the message store.
595+
store.dispose();
596+
check(store.debugMessageStore.debugMessageLists).isEmpty();
597+
});
579598
}

test/model/store_test.dart

Lines changed: 32 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,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)