Skip to content

Commit 03dfdf1

Browse files
committed
store: Implement removeAccount
Related: #463
1 parent fd3fe81 commit 03dfdf1

File tree

4 files changed

+135
-7
lines changed

4 files changed

+135
-7
lines changed

lib/model/store.dart

+39-2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ abstract class GlobalStore extends ChangeNotifier {
7878
}
7979

8080
final Map<int, PerAccountStore> _perAccountStores = {};
81+
82+
int get debugNumPerAccountStoresLoading => _perAccountStoresLoading.length;
8183
final Map<int, Future<PerAccountStore>> _perAccountStoresLoading = {};
8284

8385
/// The store's per-account data for the given account, if already loaded.
@@ -144,8 +146,15 @@ abstract class GlobalStore extends ChangeNotifier {
144146
/// This method should be called only by the implementation of [perAccount].
145147
/// Other callers interested in per-account data should use [perAccount]
146148
/// 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+
store.dispose();
155+
throw AccountNotFoundException();
156+
}
157+
return store;
149158
}
150159

151160
/// Load per-account data for the given account, unconditionally.
@@ -199,10 +208,26 @@ abstract class GlobalStore extends ChangeNotifier {
199208
/// Update an account in the underlying data store.
200209
Future<void> doUpdateAccount(int accountId, AccountsCompanion data);
201210

211+
/// Remove an account from the store.
212+
Future<void> removeAccount(int accountId) async {
213+
assert(_accounts.containsKey(accountId));
214+
await doRemoveAccount(accountId);
215+
if (!_accounts.containsKey(accountId)) return; // Already removed.
216+
_accounts.remove(accountId);
217+
_perAccountStores.remove(accountId)?.dispose();
218+
unawaited(_perAccountStoresLoading.remove(accountId));
219+
notifyListeners();
220+
}
221+
222+
/// Remove an account from the underlying data store.
223+
Future<void> doRemoveAccount(int accountId);
224+
202225
@override
203226
String toString() => '${objectRuntimeType(this, 'GlobalStore')}#${shortHash(this)}';
204227
}
205228

229+
class AccountNotFoundException implements Exception {}
230+
206231
/// Store for the user's data for a given Zulip account.
207232
///
208233
/// This should always have a consistent snapshot of the state on the server,
@@ -376,6 +401,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
376401
// Data attached to the self-account on the realm.
377402

378403
final int accountId;
404+
405+
/// The [Account] this store belongs to.
406+
///
407+
/// Will throw if called after [dispose] has been called.
379408
Account get account => _globalStore.getAccount(accountId)!;
380409

381410
/// Always equal to `account.userId`.
@@ -733,6 +762,14 @@ class LiveGlobalStore extends GlobalStore {
733762
assert(rowsAffected == 1);
734763
}
735764

765+
@override
766+
Future<void> doRemoveAccount(int accountId) async {
767+
final rowsAffected = await (_db.delete(_db.accounts)
768+
..where((a) => a.id.equals(accountId))
769+
).go();
770+
assert(rowsAffected == 1);
771+
}
772+
736773
@override
737774
String toString() => '${objectRuntimeType(this, 'LiveGlobalStore')}#${shortHash(this)}';
738775
}

lib/widgets/store.dart

+13-5
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,19 @@ 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+
}
217+
}();
210218
}
211219
}
212220

test/model/store_test.dart

+78
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,84 @@ void main() {
166166
// TODO test database gets updated correctly (an integration test with sqlite?)
167167
});
168168

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

test/model/test_store.dart

+5
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ class TestGlobalStore extends GlobalStore {
119119
// Nothing to do.
120120
}
121121

122+
@override
123+
Future<void> doRemoveAccount(int accountId) async {
124+
// Nothing to do.
125+
}
126+
122127
@override
123128
Future<PerAccountStore> doLoadPerAccount(int accountId) {
124129
final initialSnapshot = _initialSnapshots[accountId]!;

0 commit comments

Comments
 (0)