Skip to content

Commit d728c8e

Browse files
committed
store: Implement removeAccount
1 parent db9ad4b commit d728c8e

File tree

4 files changed

+173
-9
lines changed

4 files changed

+173
-9
lines changed

lib/model/store.dart

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ abstract class GlobalStore extends ChangeNotifier {
7575
email: account.email, apiKey: account.apiKey);
7676
}
7777

78+
Map<int, PerAccountStore> get debugPerAccountStores => _perAccountStores;
7879
final Map<int, PerAccountStore> _perAccountStores = {};
80+
81+
Map<int, Future<PerAccountStore>> get debugPerAccountStoresLoading => _perAccountStoresLoading;
7982
final Map<int, Future<PerAccountStore>> _perAccountStoresLoading = {};
8083

8184
/// The store's per-account data for the given account, if already loaded.
@@ -142,8 +145,16 @@ abstract class GlobalStore extends ChangeNotifier {
142145
/// This method should be called only by the implementation of [perAccount].
143146
/// Other callers interested in per-account data should use [perAccount]
144147
/// and/or [perAccountSync].
145-
Future<PerAccountStore> loadPerAccount(int accountId) {
146-
return doLoadPerAccount(accountId);
148+
Future<PerAccountStore> loadPerAccount(int accountId) async {
149+
assert(_accounts.containsKey(accountId));
150+
final store = await doLoadPerAccount(accountId);
151+
if (!_accounts.containsKey(accountId)) {
152+
// [removeAccount] was called during [doLoadPerAccount].
153+
// TODO close connection inside `.dispose` instead (once tests can adapt)
154+
store..dispose()..connection.close();
155+
throw AccountNotFoundException();
156+
}
157+
return store;
147158
}
148159

149160
/// Load per-account data for the given account, unconditionally.
@@ -197,10 +208,27 @@ abstract class GlobalStore extends ChangeNotifier {
197208
/// Update an account in the underlying data store.
198209
Future<void> doUpdateAccount(int accountId, AccountsCompanion data);
199210

211+
/// Remove an account from the store.
212+
Future<void> removeAccount(int accountId) async {
213+
await doRemoveAccount(accountId);
214+
assert(_accounts.containsKey(accountId));
215+
_accounts.remove(accountId);
216+
_perAccountStores.remove(accountId)
217+
// TODO close connection inside `.dispose` instead (once tests can adapt)
218+
?..dispose()..connection.close();
219+
_perAccountStoresLoading.remove(accountId);
220+
notifyListeners();
221+
}
222+
223+
/// Remove an account from the underlying data store.
224+
Future<void> doRemoveAccount(int accountId);
225+
200226
@override
201227
String toString() => '${objectRuntimeType(this, 'GlobalStore')}#${shortHash(this)}';
202228
}
203229

230+
class AccountNotFoundException implements Exception {}
231+
204232
/// Store for the user's data for a given Zulip account.
205233
///
206234
/// This should always have a consistent snapshot of the state on the server,
@@ -337,8 +365,17 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
337365
// Data attached to the self-account on the realm.
338366

339367
final int accountId;
368+
369+
/// The [Account] this store belongs to.
370+
///
371+
/// Assumes the account hasn't been logged out.
372+
/// If the account may have been logged out, don't use this;
373+
/// use [maybeAccount] instead.
340374
Account get account => _globalStore.getAccount(accountId)!;
341375

376+
/// Like [account], but for contexts where the account might be logged out.
377+
Account? get maybeAccount => _globalStore.getAccount(accountId);
378+
342379
/// Always equal to `account.userId`.
343380
final int selfUserId;
344381

@@ -426,6 +463,9 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
426463
}
427464

428465
Future<void> handleEvent(Event event) async {
466+
// Do nothing if the account has been logged out.
467+
if (maybeAccount == null) return;
468+
429469
switch (event) {
430470
case HeartbeatEvent():
431471
assert(debugLog("server event: heartbeat"));
@@ -566,10 +606,13 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
566606
}
567607
}
568608

569-
Future<void> sendMessage({required MessageDestination destination, required String content}) {
609+
Future<void> sendMessage({required MessageDestination destination, required String content}) async {
610+
// Do nothing if the account has been logged out.
611+
if (maybeAccount == null) return;
612+
570613
// TODO implement outbox; see design at
571614
// https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/.23M3881.20Sending.20outbox.20messages.20is.20fraught.20with.20issues/near/1405739
572-
return _apiSendMessage(connection,
615+
await _apiSendMessage(connection,
573616
destination: destination,
574617
content: content,
575618
readBySender: true,
@@ -685,6 +728,14 @@ class LiveGlobalStore extends GlobalStore {
685728
assert(rowsAffected == 1);
686729
}
687730

731+
@override
732+
Future<void> doRemoveAccount(int accountId) async {
733+
final rowsAffected = await (_db.delete(_db.accounts)
734+
..where((a) => a.id.equals(accountId))
735+
).go();
736+
assert(rowsAffected == 1);
737+
}
738+
688739
@override
689740
String toString() => '${objectRuntimeType(this, 'LiveGlobalStore')}#${shortHash(this)}';
690741
}
@@ -793,11 +844,17 @@ class UpdateMachine {
793844
}());
794845
}
795846

847+
// Abort if the account has been logged out.
848+
if (store.maybeAccount == null) return;
849+
796850
final GetEventsResult result;
797851
try {
798852
result = await getEvents(store.connection,
799853
queueId: queueId, lastEventId: lastEventId);
800854
} catch (e) {
855+
// Abort if the account has been logged out.
856+
if (store.maybeAccount == null) return;
857+
801858
store.isLoading = true;
802859
switch (e) {
803860
case ZulipApiException(code: 'BAD_EVENT_QUEUE_ID'):
@@ -825,6 +882,9 @@ class UpdateMachine {
825882
}
826883
}
827884

885+
// Abort if the account has been logged out.
886+
if (store.maybeAccount == null) return;
887+
828888
// After one successful request, we reset backoff to its initial state.
829889
// That way if the user is off the network and comes back on, the app
830890
// doesn't wind up in a state where it's slow to recover the next time
@@ -847,6 +907,10 @@ class UpdateMachine {
847907
for (final event in events) {
848908
await store.handleEvent(event);
849909
}
910+
911+
// Abort if the account has been logged out.
912+
if (store.maybeAccount == null) return;
913+
850914
if (events.isNotEmpty) {
851915
lastEventId = events.last.id;
852916
}
@@ -881,6 +945,9 @@ class UpdateMachine {
881945
// TODO(#322) save acked token, to dedupe updating it on the server
882946
// TODO(#323) track the registerFcmToken/etc request, warn if not succeeding
883947
Future<void> registerNotificationToken() async {
948+
// Abort if the account has been logged out.
949+
if (store.maybeAccount == null) return;
950+
884951
if (!debugEnableRegisterNotificationToken) {
885952
return;
886953
}

lib/widgets/store.dart

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,21 @@ class _PerAccountStoreWidgetState extends State<PerAccountStoreWidget> {
202202
_setStore(store);
203203
} else {
204204
// If we don't already have data, request it.
205-
206-
// If this succeeds, globalStore will notify listeners, and
207-
// [didChangeDependencies] will run again, this time in the
208-
// `store != null` case above.
209-
globalStore.perAccount(widget.accountId);
205+
() async {
206+
try {
207+
// If this succeeds, globalStore will notify listeners, and
208+
// [didChangeDependencies] will run again, this time in the
209+
// `store != null` case above.
210+
await globalStore.perAccount(widget.accountId);
211+
} on AccountNotFoundException {
212+
// The account was logged out while its store was loading.
213+
// This widget will be showing [placeholder] perpetually,
214+
// but that's OK as long as other code will be removing it from the UI
215+
// (for example by removing a per-account route from the nav).
216+
} catch (e) {
217+
// TODO(log)
218+
}
219+
}();
210220
}
211221
}
212222

test/model/store_test.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,88 @@ void main() {
164164
// TODO test database gets updated correctly (an integration test with sqlite?)
165165
});
166166

167+
group('GlobalStore.removeAccount', () {
168+
void checkGlobalStore(GlobalStore store, int accountId, {
169+
required bool expectAccount,
170+
required bool expectStore,
171+
bool expectStoreLoading = false,
172+
}) {
173+
expectAccount
174+
? check(store.getAccount(accountId)).isNotNull()
175+
: check(store.getAccount(accountId)).isNull();
176+
expectStore
177+
? check(store.debugPerAccountStores[accountId]).isNotNull()
178+
: check(store.debugPerAccountStores[accountId]).isNull();
179+
expectStoreLoading
180+
? check(store.debugPerAccountStoresLoading[accountId]).isNotNull()
181+
: check(store.debugPerAccountStoresLoading[accountId]).isNull();
182+
}
183+
184+
test('when store loaded', () async {
185+
final globalStore = eg.globalStore();
186+
await globalStore.add(eg.selfAccount, eg.initialSnapshot());
187+
await globalStore.perAccount(eg.selfAccount.id);
188+
189+
checkGlobalStore(globalStore, eg.selfAccount.id,
190+
expectAccount: true, expectStore: true);
191+
int notifyCount = 0;
192+
globalStore.addListener(() => notifyCount++);
193+
194+
await globalStore.removeAccount(eg.selfAccount.id);
195+
196+
// TODO test that the removed store got disposed and its connection closed
197+
checkGlobalStore(globalStore, eg.selfAccount.id,
198+
expectAccount: false, expectStore: false);
199+
check(notifyCount).equals(1);
200+
});
201+
202+
test('when store not loaded', () async {
203+
final globalStore = eg.globalStore();
204+
await globalStore.add(eg.selfAccount, eg.initialSnapshot());
205+
206+
checkGlobalStore(globalStore, eg.selfAccount.id,
207+
expectAccount: true, expectStore: false);
208+
int notifyCount = 0;
209+
globalStore.addListener(() => notifyCount++);
210+
211+
await globalStore.removeAccount(eg.selfAccount.id);
212+
213+
checkGlobalStore(globalStore, eg.selfAccount.id,
214+
expectAccount: false, expectStore: false);
215+
check(notifyCount).equals(1);
216+
});
217+
218+
test('when store loading', () async {
219+
final globalStore = LoadingTestGlobalStore(accounts: [eg.selfAccount]);
220+
checkGlobalStore(globalStore, eg.selfAccount.id,
221+
expectAccount: true, expectStore: false, expectStoreLoading: false);
222+
223+
// don't await; we'll complete/await it manually after removeAccount
224+
final loadingFuture = globalStore.perAccount(eg.selfAccount.id);
225+
226+
checkGlobalStore(globalStore, eg.selfAccount.id,
227+
expectAccount: true, expectStore: false, expectStoreLoading: true);
228+
int notifyCount = 0;
229+
globalStore.addListener(() => notifyCount++);
230+
231+
await globalStore.removeAccount(eg.selfAccount.id);
232+
233+
checkGlobalStore(globalStore, eg.selfAccount.id,
234+
expectAccount: false, expectStore: false, expectStoreLoading: false);
235+
check(notifyCount).equals(1);
236+
237+
globalStore.completers[eg.selfAccount.id]!.single
238+
.complete(eg.store(account: eg.selfAccount, initialSnapshot: eg.initialSnapshot()));
239+
// TODO test that the never-used store got disposed and its connection closed
240+
check(loadingFuture).throws<AccountNotFoundException>();
241+
checkGlobalStore(globalStore, eg.selfAccount.id,
242+
expectAccount: false, expectStore: false, expectStoreLoading: false);
243+
check(notifyCount).equals(1); // no extra notify
244+
});
245+
246+
// TODO test database gets updated correctly (an integration test with sqlite?)
247+
});
248+
167249
group('PerAccountStore.handleEvent', () {
168250
// Mostly this method just dispatches to ChannelStore and MessageStore etc.,
169251
// and so most of the tests live in the test files for those

test/model/test_store.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ class TestGlobalStore extends GlobalStore {
105105
// Nothing to do.
106106
}
107107

108+
@override
109+
Future<void> doRemoveAccount(int accountId) async {
110+
// Nothing to do.
111+
}
112+
108113
@override
109114
Future<PerAccountStore> doLoadPerAccount(int accountId) {
110115
final initialSnapshot = _initialSnapshots[accountId]!;

0 commit comments

Comments
 (0)