Skip to content

Commit 075247a

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: zulip#810 Signed-off-by: Zixuan James Li <[email protected]>
1 parent e91694f commit 075247a

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,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);

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
@@ -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

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)