@@ -78,6 +78,8 @@ abstract class GlobalStore extends ChangeNotifier {
78
78
}
79
79
80
80
final Map <int , PerAccountStore > _perAccountStores = {};
81
+
82
+ int get debugNumPerAccountStoresLoading => _perAccountStoresLoading.length;
81
83
final Map <int , Future <PerAccountStore >> _perAccountStoresLoading = {};
82
84
83
85
/// The store's per-account data for the given account, if already loaded.
@@ -144,8 +146,16 @@ abstract class GlobalStore extends ChangeNotifier {
144
146
/// This method should be called only by the implementation of [perAccount] .
145
147
/// Other callers interested in per-account data should use [perAccount]
146
148
/// and/or [perAccountSync] .
147
- Future <PerAccountStore > loadPerAccount (int accountId) {
148
- return doLoadPerAccount (accountId);
149
+ Future <PerAccountStore > loadPerAccount (int accountId) async {
150
+ assert (_accounts.containsKey (accountId));
151
+ final store = await doLoadPerAccount (accountId);
152
+ if (! _accounts.containsKey (accountId)) {
153
+ // [removeAccount] was called during [doLoadPerAccount].
154
+ // TODO close connection inside `.dispose` instead (once tests can adapt)
155
+ store..dispose ()..connection.close ();
156
+ throw AccountNotFoundException ();
157
+ }
158
+ return store;
149
159
}
150
160
151
161
/// Load per-account data for the given account, unconditionally.
@@ -199,10 +209,27 @@ abstract class GlobalStore extends ChangeNotifier {
199
209
/// Update an account in the underlying data store.
200
210
Future <void > doUpdateAccount (int accountId, AccountsCompanion data);
201
211
212
+ /// Remove an account from the store.
213
+ Future <void > removeAccount (int accountId) async {
214
+ await doRemoveAccount (accountId);
215
+ assert (_accounts.containsKey (accountId));
216
+ _accounts.remove (accountId);
217
+ _perAccountStores.remove (accountId)
218
+ // TODO close connection inside `.dispose` instead (once tests can adapt)
219
+ ? ..dispose ()..connection.close ();
220
+ unawaited (_perAccountStoresLoading.remove (accountId));
221
+ notifyListeners ();
222
+ }
223
+
224
+ /// Remove an account from the underlying data store.
225
+ Future <void > doRemoveAccount (int accountId);
226
+
202
227
@override
203
228
String toString () => '${objectRuntimeType (this , 'GlobalStore' )}#${shortHash (this )}' ;
204
229
}
205
230
231
+ class AccountNotFoundException implements Exception {}
232
+
206
233
/// Store for the user's data for a given Zulip account.
207
234
///
208
235
/// This should always have a consistent snapshot of the state on the server,
@@ -375,6 +402,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
375
402
// Data attached to the self-account on the realm.
376
403
377
404
final int accountId;
405
+
406
+ /// The [Account] this store belongs to.
407
+ ///
408
+ /// Will throw if called after [dispose] has been called.
378
409
Account get account => _globalStore.getAccount (accountId)! ;
379
410
380
411
/// Always equal to `account.userId` .
@@ -444,6 +475,8 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
444
475
// End of data.
445
476
////////////////////////////////////////////////////////////////
446
477
478
+ bool _disposed = false ;
479
+
447
480
/// Called when the app is reassembled during debugging, e.g. for hot reload.
448
481
///
449
482
/// This will redo from scratch any computations we can, such as parsing
@@ -461,9 +494,12 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
461
494
typingStatus.dispose ();
462
495
updateMachine? .dispose (); // TODO is updateMachine ever null except in tests?
463
496
super .dispose ();
497
+ _disposed = true ;
464
498
}
465
499
466
500
Future <void > handleEvent (Event event) async {
501
+ assert (! _disposed);
502
+
467
503
switch (event) {
468
504
case HeartbeatEvent ():
469
505
assert (debugLog ("server event: heartbeat" ));
@@ -604,10 +640,12 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
604
640
}
605
641
}
606
642
607
- Future <void > sendMessage ({required MessageDestination destination, required String content}) {
643
+ Future <void > sendMessage ({required MessageDestination destination, required String content}) async {
644
+ assert (! _disposed);
645
+
608
646
// TODO implement outbox; see design at
609
647
// https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/.23M3881.20Sending.20outbox.20messages.20is.20fraught.20with.20issues/near/1405739
610
- return _apiSendMessage (connection,
648
+ await _apiSendMessage (connection,
611
649
destination: destination,
612
650
content: content,
613
651
readBySender: true ,
@@ -723,6 +761,14 @@ class LiveGlobalStore extends GlobalStore {
723
761
assert (rowsAffected == 1 );
724
762
}
725
763
764
+ @override
765
+ Future <void > doRemoveAccount (int accountId) async {
766
+ final rowsAffected = await (_db.delete (_db.accounts)
767
+ ..where ((a) => a.id.equals (accountId))
768
+ ).go ();
769
+ assert (rowsAffected == 1 );
770
+ }
771
+
726
772
@override
727
773
String toString () => '${objectRuntimeType (this , 'LiveGlobalStore' )}#${shortHash (this )}' ;
728
774
}
@@ -789,6 +835,8 @@ class UpdateMachine {
789
835
final String queueId;
790
836
int lastEventId;
791
837
838
+ bool _disposed = false ;
839
+
792
840
static Future <InitialSnapshot > _registerQueueWithRetry (
793
841
ApiConnection connection) async {
794
842
BackoffMachine ? backoffMachine;
@@ -875,11 +923,15 @@ class UpdateMachine {
875
923
}());
876
924
}
877
925
926
+ if (_disposed) return ;
927
+
878
928
final GetEventsResult result;
879
929
try {
880
930
result = await getEvents (store.connection,
881
931
queueId: queueId, lastEventId: lastEventId);
882
932
} catch (e) {
933
+ if (_disposed) return ;
934
+
883
935
store.isLoading = true ;
884
936
switch (e) {
885
937
case ZulipApiException (code: 'BAD_EVENT_QUEUE_ID' ):
@@ -907,6 +959,8 @@ class UpdateMachine {
907
959
}
908
960
}
909
961
962
+ if (_disposed) return ;
963
+
910
964
// After one successful request, we reset backoff to its initial state.
911
965
// That way if the user is off the network and comes back on, the app
912
966
// doesn't wind up in a state where it's slow to recover the next time
@@ -928,6 +982,7 @@ class UpdateMachine {
928
982
final events = result.events;
929
983
for (final event in events) {
930
984
await store.handleEvent (event);
985
+ if (_disposed) return ;
931
986
}
932
987
if (events.isNotEmpty) {
933
988
lastEventId = events.last.id;
@@ -943,6 +998,8 @@ class UpdateMachine {
943
998
// TODO(#322) save acked token, to dedupe updating it on the server
944
999
// TODO(#323) track the registerFcmToken/etc request, warn if not succeeding
945
1000
Future <void > registerNotificationToken () async {
1001
+ if (_disposed) return ;
1002
+
946
1003
if (! debugEnableRegisterNotificationToken) {
947
1004
return ;
948
1005
}
@@ -958,6 +1015,7 @@ class UpdateMachine {
958
1015
959
1016
void dispose () { // TODO abort long-poll and close ApiConnection
960
1017
NotificationService .instance.token.removeListener (_registerNotificationToken);
1018
+ _disposed = true ;
961
1019
}
962
1020
963
1021
/// In debug mode, controls whether [fetchEmojiData] should
0 commit comments