Skip to content

Commit

Permalink
Add clear and clearAll functionality (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
Abdktefane authored Nov 17, 2022
1 parent 4e27936 commit fbe45b0
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 35 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Generally you will implement the `SourceOfTruth` using a local database. However
final sourceOfTruth = SourceOfTruth<String, List<Tweet>>(
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
);
```

Expand Down Expand Up @@ -117,16 +119,27 @@ 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
final List<Tweet> freshTweets = await stock.fresh(key);
// Get the previous cached data
final List<Tweet> 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.
Expand Down
10 changes: 9 additions & 1 deletion lib/src/implementations/source_of_truth_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
import 'package:stock/src/source_of_truth.dart';

class SourceOfTruthImpl<Key, T> implements SourceOfTruth<Key, T> {
SourceOfTruthImpl(this._reader, this._writer);
SourceOfTruthImpl(this._reader, this._writer, this._delete, this._deleteAll);

final Stream<T?> Function(Key key) _reader;
final Future<void> Function(Key key, T? output) _writer;
final Future<void> Function(Key key)? _delete;
final Future<void> Function()? _deleteAll;

@override
Stream<T?> reader(Key key) => _reader(key);

@override
Future<void> write(Key key, T? value) => _writer(key, value);

@override
Future<void> delete(Key key) async => await _delete?.call(key);

@override
Future<void> deleteAll() async => await _deleteAll?.call();
}

class WriteWrappedSourceOfTruth<Key, T> extends CachedSourceOfTruth<Key, T> {
Expand Down
6 changes: 6 additions & 0 deletions lib/src/implementations/stock_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class StockImpl<Key, Output> implements Stock<Key, Output> {
),
);

@override
Future<void> clear(Key key) async => _sourceOfTruth?.delete(key);

@override
Future<void> clearAll() async => _sourceOfTruth?.deleteAll();

Stream<StockResponse<Output>> streamFromRequest(StockRequest<Key> request) =>
_generateCombinedNetworkAndSourceOfTruthStream(
request,
Expand Down
25 changes: 23 additions & 2 deletions lib/src/source_of_truth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Key, T> {
/// 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<T?> Function(Key key) reader,
required Future<void> Function(Key key, T? output) writer,
Future<void> Function(Key key)? delete,
Future<void> 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].
Expand All @@ -61,6 +66,13 @@ abstract class SourceOfTruth<Key, T> {
/// APIs as long as you are using a local storage that supports observability
/// (e.g. Floor, Drift, Realm).
Future<void> write(Key key, T? value);

/// Used by [Stock] to delete records in the source of truth for
/// the given [key].
Future<void> delete(Key key);

/// Used by [Stock] to delete all records in the source of truth.
Future<void> deleteAll();
}

/// A memory cache implementation of a [SourceOfTruth], which stores the latest
Expand Down Expand Up @@ -94,4 +106,13 @@ class CachedSourceOfTruth<Key, T> implements SourceOfTruth<Key, T> {
setCachedValue(key, value);
_streamController.add(KeyValue(key, value));
}

@override
Future<void> delete(Key key) async {
_cachedValues.remove(key);
_streamController.add(KeyValue(key, null));
}

@override
Future<void> deleteAll() async => _cachedValues.keys.toList().forEach(delete);
}
10 changes: 10 additions & 0 deletions lib/src/stock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,14 @@ abstract class Stock<Key, T> {
/// Returns data for [key] if it is cached otherwise will return
/// fresh/network data (updating your cache).
Future<T> 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<void> 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<void> clearAll();
}
14 changes: 7 additions & 7 deletions scripts/checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
46 changes: 46 additions & 0 deletions test/clear_and_clear_all_test.dart
Original file line number Diff line number Diff line change
@@ -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<int, int>();

final fetcher = MockFutureFetcher<int, int>();
var timesCalled = 0;
when(fetcher.factory).thenReturn((key) => Stream.value(++timesCalled));

final stock = Stock<int, int>(
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<int, int>();

final fetcher = MockFutureFetcher<int, int>();
var timesCalled = 0;
when(fetcher.factory).thenReturn((key) => Stream.value(++timesCalled));

final stock = Stock<int, int>(
fetcher: fetcher,
sourceOfTruth: sourceOfTruth,
);

await stock.clearAll();

verify(sourceOfTruth.deleteAll()).called(1);
expect(timesCalled, equals(0));
});
});
}
101 changes: 76 additions & 25 deletions test/common_mocks.mocks.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit fbe45b0

Please sign in to comment.