From fbe45b03dbf234edb9ba3ef790a50a8697a41c84 Mon Sep 17 00:00:00 2001 From: Abd al-Rahman al-Ktefane <48357717+Abdktefane@users.noreply.github.com> Date: Fri, 18 Nov 2022 01:50:11 +0300 Subject: [PATCH] Add clear and clearAll functionality (#30) --- README.md | 13 +++ .../implementations/source_of_truth_impl.dart | 10 +- lib/src/implementations/stock_impl.dart | 6 ++ lib/src/source_of_truth.dart | 25 ++++- lib/src/stock.dart | 10 ++ scripts/checks.sh | 14 +-- test/clear_and_clear_all_test.dart | 46 ++++++++ test/common_mocks.mocks.dart | 101 +++++++++++++----- test/source_of_truth_test.dart | 42 ++++++++ 9 files changed, 232 insertions(+), 35 deletions(-) create mode 100644 test/clear_and_clear_all_test.dart diff --git a/README.md b/README.md index 75d0e54..be68308 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ Generally you will implement the `SourceOfTruth` using a local database. However final sourceOfTruth = SourceOfTruth>( reader: (userId) => _database.getUserTweets(userId), writer: (userId, tweets) => _database.writeUserTweets(userId, tweets), + delete: (userId) => _database.deleteUserTweets(userId), // this is optional + deleteAll: () => _database.deleteAllTweets(), // this is optional ); ``` @@ -117,6 +119,9 @@ Stock provides a couple of methods to get data without using a data stream. 1. `get` returns cached data -if it is cached- otherwise will return fresh/network data (updating your caches). 2. `fresh` returns fresh data updating your cache +3. `clear` purge a particular entry from memory and disk cache +4. `clearAll` Purge all entries from memory and disk cache + ```dart // Get fresh data @@ -124,9 +129,17 @@ Stock provides a couple of methods to get data without using a data stream. // Get the previous cached data final List cachedTweets = await stock.get(key); + + // Clear key from stock + await stock.clear(key); + + // Clear all keys from stock + await stock.clearAll(); ``` + + ### Use different types for `Fetcher` and `SourceOfTruth` Sometimes you need to use different entities for Network and DB. For that case `Stock` provides the `StockTypeMapper`, a class that transforms one entity into the other. diff --git a/lib/src/implementations/source_of_truth_impl.dart b/lib/src/implementations/source_of_truth_impl.dart index fc53bde..85120ba 100644 --- a/lib/src/implementations/source_of_truth_impl.dart +++ b/lib/src/implementations/source_of_truth_impl.dart @@ -3,16 +3,24 @@ import 'package:stock/src/source_of_truth.dart'; class SourceOfTruthImpl implements SourceOfTruth { - SourceOfTruthImpl(this._reader, this._writer); + SourceOfTruthImpl(this._reader, this._writer, this._delete, this._deleteAll); final Stream Function(Key key) _reader; final Future Function(Key key, T? output) _writer; + final Future Function(Key key)? _delete; + final Future Function()? _deleteAll; @override Stream reader(Key key) => _reader(key); @override Future write(Key key, T? value) => _writer(key, value); + + @override + Future delete(Key key) async => await _delete?.call(key); + + @override + Future deleteAll() async => await _deleteAll?.call(); } class WriteWrappedSourceOfTruth extends CachedSourceOfTruth { diff --git a/lib/src/implementations/stock_impl.dart b/lib/src/implementations/stock_impl.dart index 2387419..f493a8c 100644 --- a/lib/src/implementations/stock_impl.dart +++ b/lib/src/implementations/stock_impl.dart @@ -54,6 +54,12 @@ class StockImpl implements Stock { ), ); + @override + Future clear(Key key) async => _sourceOfTruth?.delete(key); + + @override + Future clearAll() async => _sourceOfTruth?.deleteAll(); + Stream> streamFromRequest(StockRequest request) => _generateCombinedNetworkAndSourceOfTruthStream( request, diff --git a/lib/src/source_of_truth.dart b/lib/src/source_of_truth.dart index 2c2b21d..d9374b2 100644 --- a/lib/src/source_of_truth.dart +++ b/lib/src/source_of_truth.dart @@ -38,15 +38,20 @@ import 'package:stock/src/stock_extensions.dart'; /// For this case you can use the [mapTo] and [mapToUsingMapper] extensions. /// abstract class SourceOfTruth { - /// Creates a source of truth that is accessed via [reader] and [writer]. + /// Creates a source of truth that is accessed via [reader], [writer], + /// [delete] and [deleteAll]. /// /// The [reader] function is used to read records from the source of truth /// The [writer] function is used to write records to the source of truth + /// The [delete] function for deleting records in the source of truth. + /// The [deleteAll] function for deleting all records in the source of truth factory SourceOfTruth({ required Stream Function(Key key) reader, required Future Function(Key key, T? output) writer, + Future Function(Key key)? delete, + Future Function()? deleteAll, }) => - SourceOfTruthImpl(reader, writer); + SourceOfTruthImpl(reader, writer, delete, deleteAll); /// Used by [Stock] to read records from the source of truth for the given /// [key]. @@ -61,6 +66,13 @@ abstract class SourceOfTruth { /// APIs as long as you are using a local storage that supports observability /// (e.g. Floor, Drift, Realm). Future write(Key key, T? value); + + /// Used by [Stock] to delete records in the source of truth for + /// the given [key]. + Future delete(Key key); + + /// Used by [Stock] to delete all records in the source of truth. + Future deleteAll(); } /// A memory cache implementation of a [SourceOfTruth], which stores the latest @@ -94,4 +106,13 @@ class CachedSourceOfTruth implements SourceOfTruth { setCachedValue(key, value); _streamController.add(KeyValue(key, value)); } + + @override + Future delete(Key key) async { + _cachedValues.remove(key); + _streamController.add(KeyValue(key, null)); + } + + @override + Future deleteAll() async => _cachedValues.keys.toList().forEach(delete); } diff --git a/lib/src/stock.dart b/lib/src/stock.dart index 0058b80..a4580e2 100644 --- a/lib/src/stock.dart +++ b/lib/src/stock.dart @@ -36,4 +36,14 @@ abstract class Stock { /// Returns data for [key] if it is cached otherwise will return /// fresh/network data (updating your cache). Future get(Key key); + + /// Purge a particular entry from memory and disk cache. + /// Persistent storage will only be cleared if a delete function was passed to + /// SourceOfTruth.delete when creating the [Stock]. + Future clear(Key key); + + /// Purge all entries from memory and disk cache. + /// Persistent storage will only be cleared if a deleteAll function was passed + /// to SourceOfTruth.delete when creating the [Stock]. + Future clearAll(); } diff --git a/scripts/checks.sh b/scripts/checks.sh index a3bb47a..3928fdd 100755 --- a/scripts/checks.sh +++ b/scripts/checks.sh @@ -2,24 +2,24 @@ RED='\033[0;31m' echo ':: Get dependencies ::' -dart pub get +fvm dart pub get echo ':: Check code format ::' -dart format --set-exit-if-changed . || { echo -e "${RED}Invalid format" ; exit 1; } +fvm dart format --set-exit-if-changed . || { echo -e "${RED}Invalid format" ; exit 1; } echo ':: Run dart linter ::' -dart analyze --fatal-infos || { echo -e "${RED}Linter error" ; exit 1; } +fvm dart analyze --fatal-infos || { echo -e "${RED}Linter error" ; exit 1; } echo ':: Run flutter linter ::' -flutter analyze --fatal-infos || { echo -e "${RED}Linter error" ; exit 1; } +fvm flutter analyze --fatal-infos || { echo -e "${RED}Linter error" ; exit 1; } result=$(dart run dart_code_metrics:metrics analyze lib --fatal-style --fatal-performance --fatal-warnings) echo "$result" [[ $result == '✔ no issues found!' ]] || { echo -e "${RED}Linter error" ; exit 1; } -dart run dart_code_metrics:metrics check-unused-code lib --fatal-unused || { echo -e "${RED}Linter error" ; exit 1; } +fvm dart run dart_code_metrics:metrics check-unused-code lib --fatal-unused || { echo -e "${RED}Linter error" ; exit 1; } -dart run dart_code_metrics:metrics check-unused-files lib --fatal-unused || { echo -e "${RED}Linter error" ; exit 1; } +fvm dart run dart_code_metrics:metrics check-unused-files lib --fatal-unused || { echo -e "${RED}Linter error" ; exit 1; } echo ':: Run tests ::' -dart test || { echo -e "${RED}Test error" ; exit 1; } +fvm dart test || { echo -e "${RED}Test error" ; exit 1; } diff --git a/test/clear_and_clear_all_test.dart b/test/clear_and_clear_all_test.dart new file mode 100644 index 0000000..7026104 --- /dev/null +++ b/test/clear_and_clear_all_test.dart @@ -0,0 +1,46 @@ +import 'package:mockito/mockito.dart'; +import 'package:stock/src/stock.dart'; +import 'package:test/test.dart'; + +import 'common/source_of_truth/cached_and_mocked_source_of_truth.dart'; +import 'common_mocks.mocks.dart'; + +void main() { + group('Clear tests', () { + test('Stock Clear invokes SOT clear', () async { + final sourceOfTruth = createMockedSourceOfTruth(); + + final fetcher = MockFutureFetcher(); + var timesCalled = 0; + when(fetcher.factory).thenReturn((key) => Stream.value(++timesCalled)); + + final stock = Stock( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + await stock.clear(1); + verify(sourceOfTruth.delete(1)).called(1); + expect(timesCalled, equals(0)); + }); + }); + group('ClearAll tests', () { + test('Stock ClearAll invokes SOT clearAll', () async { + final sourceOfTruth = createMockedSourceOfTruth(); + + final fetcher = MockFutureFetcher(); + var timesCalled = 0; + when(fetcher.factory).thenReturn((key) => Stream.value(++timesCalled)); + + final stock = Stock( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + await stock.clearAll(); + + verify(sourceOfTruth.deleteAll()).called(1); + expect(timesCalled, equals(0)); + }); + }); +} diff --git a/test/common_mocks.mocks.dart b/test/common_mocks.mocks.dart index bac4229..ec07837 100644 --- a/test/common_mocks.mocks.dart +++ b/test/common_mocks.mocks.dart @@ -31,8 +31,13 @@ class MockCallbackVoid extends _i1.Mock implements _i2.CallbackVoid { } @override - void call() => super.noSuchMethod(Invocation.method(#call, []), - returnValueForMissingStub: null); + void call() => super.noSuchMethod( + Invocation.method( + #call, + [], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [CallbackInt]. @@ -44,8 +49,13 @@ class MockCallbackInt extends _i1.Mock implements _i2.CallbackInt { } @override - int call() => - (super.noSuchMethod(Invocation.method(#call, []), returnValue: 0) as int); + int call() => (super.noSuchMethod( + Invocation.method( + #call, + [], + ), + returnValue: 0, + ) as int); } /// A class which mocks [FutureFetcher]. @@ -58,14 +68,18 @@ class MockFutureFetcher extends _i1.Mock } @override - _i4.Stream Function(Key) get factory => - (super.noSuchMethod(Invocation.getter(#factory), - returnValue: (Key key) => _i4.Stream.empty()) - as _i4.Stream Function(Key)); + _i4.Stream Function(Key) get factory => (super.noSuchMethod( + Invocation.getter(#factory), + returnValue: (Key key) => _i4.Stream.empty(), + ) as _i4.Stream Function(Key)); @override - set factory(_i4.Stream Function(Key)? _factory) => - super.noSuchMethod(Invocation.setter(#factory, _factory), - returnValueForMissingStub: null); + set factory(_i4.Stream Function(Key)? _factory) => super.noSuchMethod( + Invocation.setter( + #factory, + _factory, + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [StreamFetcher]. @@ -78,14 +92,18 @@ class MockStreamFetcher extends _i1.Mock } @override - _i4.Stream Function(Key) get factory => - (super.noSuchMethod(Invocation.getter(#factory), - returnValue: (Key key) => _i4.Stream.empty()) - as _i4.Stream Function(Key)); + _i4.Stream Function(Key) get factory => (super.noSuchMethod( + Invocation.getter(#factory), + returnValue: (Key key) => _i4.Stream.empty(), + ) as _i4.Stream Function(Key)); @override - set factory(_i4.Stream Function(Key)? _factory) => - super.noSuchMethod(Invocation.setter(#factory, _factory), - returnValueForMissingStub: null); + set factory(_i4.Stream Function(Key)? _factory) => super.noSuchMethod( + Invocation.setter( + #factory, + _factory, + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [SourceOfTruth]. @@ -98,12 +116,45 @@ class MockSourceOfTruth extends _i1.Mock } @override - _i4.Stream reader(Key? key) => - (super.noSuchMethod(Invocation.method(#reader, [key]), - returnValue: _i4.Stream.empty()) as _i4.Stream); + _i4.Stream reader(Key? key) => (super.noSuchMethod( + Invocation.method( + #reader, + [key], + ), + returnValue: _i4.Stream.empty(), + ) as _i4.Stream); @override - _i4.Future write(Key? key, T? value) => (super.noSuchMethod( - Invocation.method(#write, [key, value]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value()) as _i4.Future); + _i4.Future write( + Key? key, + T? value, + ) => + (super.noSuchMethod( + Invocation.method( + #write, + [ + key, + value, + ], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future delete(Key? key) => (super.noSuchMethod( + Invocation.method( + #delete, + [key], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future deleteAll() => (super.noSuchMethod( + Invocation.method( + #deleteAll, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } diff --git a/test/source_of_truth_test.dart b/test/source_of_truth_test.dart index 84e0583..2c153e3 100644 --- a/test/source_of_truth_test.dart +++ b/test/source_of_truth_test.dart @@ -24,5 +24,47 @@ void main() { expect(oneResult, equals([1, 2])); expect(twoResult, equals([null, 3, 4])); }); + + test('Clear SOT record with key', () async { + final sot = CachedSourceOfTruth(); + await sot.write(1, 1); + final oneResultListener = ResultListener(sot.reader(1)); + final twoResultListener = ResultListener(sot.reader(2)); + // Wait for initialization time + await Future.delayed(const Duration(milliseconds: 100)); + + await sot.write(1, 2); + await sot.write(2, 3); + await sot.write(2, 4); + await sot.delete(1); + + await Future.delayed(const Duration(milliseconds: 100)); + final oneResult = await oneResultListener.stopAndGetResult(); + final twoResult = await twoResultListener.stopAndGetResult(); + + expect(oneResult, equals([1, 2, null])); + expect(twoResult, equals([null, 3, 4])); + }); + + test('Clear SOT records', () async { + final sot = CachedSourceOfTruth(); + await sot.write(1, 1); + final oneResultListener = ResultListener(sot.reader(1)); + final twoResultListener = ResultListener(sot.reader(2)); + // Wait for initialization time + await Future.delayed(const Duration(milliseconds: 100)); + + await sot.write(1, 2); + await sot.write(2, 3); + await sot.write(2, 4); + await sot.deleteAll(); + + await Future.delayed(const Duration(milliseconds: 100)); + final oneResult = await oneResultListener.stopAndGetResult(); + final twoResult = await twoResultListener.stopAndGetResult(); + + expect(oneResult, equals([1, 2, null])); + expect(twoResult, equals([null, 3, 4, null])); + }); }); }