Nikolas Rimikis
1 year ago
9 changed files with 217 additions and 44 deletions
@ -0,0 +1,41 @@
|
||||
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; |
||||
}); |
||||
} |
||||
|
||||
/// 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,59 @@
|
||||
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); |
||||
}); |
||||
}); |
||||
} |
@ -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