Nikolas Rimikis
1 year ago
committed by
GitHub
10 changed files with 250 additions and 45 deletions
@ -0,0 +1,49 @@ |
|||||||
|
import 'package:neon/models.dart'; |
||||||
|
import 'package:neon/src/models/disposable.dart'; |
||||||
|
|
||||||
|
/// Cache for [Account] specific [Disposable] objects. |
||||||
|
class AccountCache<T extends Disposable> implements Disposable { |
||||||
|
AccountCache(); |
||||||
|
|
||||||
|
final Map<String, T> _cache = {}; |
||||||
|
|
||||||
|
/// Disposes all entries and clears the cache. |
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
_cache |
||||||
|
..disposeAll() |
||||||
|
..clear(); |
||||||
|
} |
||||||
|
|
||||||
|
/// Updates the cache against the given [accounts]. |
||||||
|
/// |
||||||
|
/// Every cache entry with an account no longer in [accounts] is removed and |
||||||
|
/// disposed. This method is in O(N²). |
||||||
|
void pruneAgainst(final Iterable<Account> accounts) { |
||||||
|
_cache.removeWhere((final key, final value) { |
||||||
|
if (accounts.tryFind(key) == null) { |
||||||
|
value.dispose(); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/// Removes [account] and its associated value, if present, from the cache. |
||||||
|
/// |
||||||
|
/// If present the value associated with `account` is disposed. |
||||||
|
void remove(final Account? account) { |
||||||
|
final removed = _cache.remove(account?.id); |
||||||
|
removed?.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
/// The value for the given [account], or `null` if [account] is not in the cache. |
||||||
|
T? operator [](final Account account) => _cache[account.id]; |
||||||
|
|
||||||
|
/// Associates the [account] with the given [value]. |
||||||
|
/// |
||||||
|
/// If the account was already in the cache, its associated value is changed. |
||||||
|
/// Otherwise the account/value pair is added to the cache. |
||||||
|
void operator []=(final Account account, final T value) => _cache[account.id] = value; |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
import 'package:meta/meta.dart'; |
||||||
|
|
||||||
|
/// Interface of a disposable class. |
||||||
|
abstract interface class Disposable { |
||||||
|
/// Discards any resources used by the object. After this is called, the |
||||||
|
/// object is not in a usable state and should be discarded (calls to |
||||||
|
/// streams or other state should throw after the object is disposed). |
||||||
|
/// |
||||||
|
/// This method should only be called by the object's owner. |
||||||
|
@mustCallSuper |
||||||
|
void dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
extension DisposeableIterableBloc on Iterable<Disposable> { |
||||||
|
/// Calls [Disposable.dispose] on all entries. |
||||||
|
/// |
||||||
|
/// The disposed values will not be removed from the iteraable. |
||||||
|
void disposeAll() { |
||||||
|
for (final bloc in this) { |
||||||
|
bloc.dispose(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension DisposeableMapBloc on Map<dynamic, Disposable> { |
||||||
|
/// Calls [Disposable.dispose] on all entries. |
||||||
|
/// |
||||||
|
/// The disposed values will not be removed from the map. |
||||||
|
/// Call [clear] to remove them. |
||||||
|
void disposeAll() { |
||||||
|
values.disposeAll(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
import 'package:mocktail/mocktail.dart'; |
||||||
|
import 'package:neon/models.dart'; |
||||||
|
import 'package:neon/src/models/account_cache.dart'; |
||||||
|
import 'package:neon/src/models/disposable.dart'; |
||||||
|
import 'package:test/test.dart'; |
||||||
|
|
||||||
|
class DisposableMock extends Mock implements Disposable {} |
||||||
|
|
||||||
|
// ignore: avoid_implementing_value_types |
||||||
|
class AccountMock extends Mock implements Account {} |
||||||
|
|
||||||
|
void main() { |
||||||
|
final disposable0 = DisposableMock(); |
||||||
|
final disposable1 = DisposableMock(); |
||||||
|
final account0 = AccountMock(); |
||||||
|
final account1 = AccountMock(); |
||||||
|
|
||||||
|
when(() => account0.id).thenReturn('key0'); |
||||||
|
when(() => account1.id).thenReturn('key1'); |
||||||
|
|
||||||
|
group('AccountCache', () { |
||||||
|
test('map functionality', () { |
||||||
|
final cache = AccountCache<DisposableMock>(); |
||||||
|
|
||||||
|
expect(cache[account0], isNull); |
||||||
|
|
||||||
|
cache[account0] = disposable0; |
||||||
|
expect(cache[account0], disposable0); |
||||||
|
|
||||||
|
expect(cache[account0] ??= disposable1, disposable0); |
||||||
|
expect(cache[account1] ??= disposable1, disposable1); |
||||||
|
}); |
||||||
|
|
||||||
|
test('prune', () { |
||||||
|
final cache = AccountCache<DisposableMock>(); |
||||||
|
cache[account0] = disposable0; |
||||||
|
cache[account1] = disposable1; |
||||||
|
|
||||||
|
cache.pruneAgainst([account0]); |
||||||
|
|
||||||
|
expect(cache[account0], disposable0); |
||||||
|
expect(cache[account1], isNull); |
||||||
|
verify(disposable1.dispose).called(1); |
||||||
|
}); |
||||||
|
|
||||||
|
test('dispose', () { |
||||||
|
final cache = AccountCache<DisposableMock>(); |
||||||
|
cache[account0] = disposable0; |
||||||
|
cache[account1] = disposable1; |
||||||
|
|
||||||
|
cache.dispose(); |
||||||
|
|
||||||
|
expect(cache[account0], isNull); |
||||||
|
expect(cache[account1], isNull); |
||||||
|
verify(disposable0.dispose).called(1); |
||||||
|
verify(disposable1.dispose).called(1); |
||||||
|
}); |
||||||
|
|
||||||
|
test('remove', () { |
||||||
|
final cache = AccountCache<DisposableMock>(); |
||||||
|
cache[account0] = disposable0; |
||||||
|
cache[account1] = disposable1; |
||||||
|
|
||||||
|
cache.remove(null); |
||||||
|
|
||||||
|
expect(cache[account0], disposable0); |
||||||
|
expect(cache[account1], disposable1); |
||||||
|
|
||||||
|
cache.remove(account0); |
||||||
|
expect(cache[account0], isNull); |
||||||
|
verify(disposable0.dispose).called(1); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
// ignore_for_file: cascade_invocations |
||||||
|
|
||||||
|
import 'package:mocktail/mocktail.dart'; |
||||||
|
import 'package:neon/src/models/disposable.dart'; |
||||||
|
import 'package:test/test.dart'; |
||||||
|
|
||||||
|
class DisposableMock extends Mock implements Disposable {} |
||||||
|
|
||||||
|
void main() { |
||||||
|
test('Disposable extensions', () { |
||||||
|
final disposable0 = DisposableMock(); |
||||||
|
final disposable1 = DisposableMock(); |
||||||
|
final disposable3 = DisposableMock(); |
||||||
|
|
||||||
|
final list = [ |
||||||
|
disposable0, |
||||||
|
disposable1, |
||||||
|
disposable3, |
||||||
|
]; |
||||||
|
|
||||||
|
list.disposeAll(); |
||||||
|
|
||||||
|
verify(disposable0.dispose).called(1); |
||||||
|
verify(disposable1.dispose).called(1); |
||||||
|
verify(disposable3.dispose).called(1); |
||||||
|
|
||||||
|
final map = { |
||||||
|
'disposable0': disposable0, |
||||||
|
'disposable1': disposable1, |
||||||
|
'disposable3': disposable3, |
||||||
|
}; |
||||||
|
|
||||||
|
map.disposeAll(); |
||||||
|
|
||||||
|
verify(disposable0.dispose).called(1); |
||||||
|
verify(disposable1.dispose).called(1); |
||||||
|
verify(disposable3.dispose).called(1); |
||||||
|
}); |
||||||
|
} |
Loading…
Reference in new issue