diff --git a/.gitignore b/.gitignore index 0c44ab0..1e2ef9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,185 @@ +# Created by https://www.toptal.com/developers/gitignore/api/dart,intellij,macos,windows +# Edit at https://www.toptal.com/developers/gitignore?templates=dart,intellij,macos,windows + +### Dart ### +# See https://www.dartlang.org/guides/libraries/private-files + # Files and directories created by pub .dart_tool/ .packages - -# Omit commiting pubspec.lock for library packages: -# https://dart.dev/guides/libraries/private-files#pubspeclock -pubspec.lock - -# Conventional directory for build outputs build/ +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock # Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. doc/api/ + +# Avoid committing generated Javascript files: +*.dart.js +*.info.json # Produced by the --dump-info flag. +*.js # When generated by dart2js. Don't specify *.js if your + # project includes source files written in JavaScript. +*.js_ +*.js.deps +*.js.map + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/dart,intellij,macos,windows \ No newline at end of file diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml deleted file mode 100644 index 32f4384..0000000 --- a/.idea/libraries/Dart_Packages.xml +++ /dev/null @@ -1,420 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml deleted file mode 100644 index 32921e3..0000000 --- a/.idea/libraries/Dart_SDK.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index e830069..ee1727b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: dart dart: - - stable - dev + - beta script: - dartanalyzer --fatal-infos --fatal-warnings ./lib ./test - dartfmt -n ./lib --set-exit-if-changed @@ -29,4 +29,4 @@ after_success: - bash <(curl -s https://codecov.io/bash) cache: directories: - - $HOME/.pub-cache \ No newline at end of file + - $HOME/.pub-cache diff --git a/lib/rx_storage.dart b/lib/rx_storage.dart index 426f68f..a7650ba 100644 --- a/lib/rx_storage.dart +++ b/lib/rx_storage.dart @@ -7,6 +7,8 @@ export 'src/impl/real_storage.dart'; export 'src/interface/rx_storage.dart'; export 'src/interface/storage.dart'; export 'src/logger/default_logger.dart'; +export 'src/logger/empty_logger.dart'; +export 'src/logger/event.dart'; export 'src/logger/logger.dart'; -export 'src/logger/logger_adapter.dart'; +export 'src/model/error.dart'; export 'src/model/key_and_value.dart'; diff --git a/lib/src/async_memoizer.dart b/lib/src/async_memoizer.dart index c05c927..6e0f40f 100644 --- a/lib/src/async_memoizer.dart +++ b/lib/src/async_memoizer.dart @@ -31,7 +31,7 @@ class AsyncMemoizer { /// /// This can be accessed at any time, and will fire once [runOnce] is called. Future get future => _completer.future; - final _completer = Completer(); + late final _completer = Completer(); /// Whether [runOnce] has been called yet. bool get hasRun => _completer.isCompleted; diff --git a/lib/src/impl/real_storage.dart b/lib/src/impl/real_storage.dart index de3336b..bccba2b 100644 --- a/lib/src/impl/real_storage.dart +++ b/lib/src/impl/real_storage.dart @@ -1,78 +1,100 @@ import 'dart:async'; -import 'dart:collection'; +import 'package:disposebag/disposebag.dart' hide Logger; import 'package:meta/meta.dart'; -import 'package:rxdart/rxdart.dart'; +import 'package:rxdart_ext/rxdart_ext.dart'; import '../async_memoizer.dart'; import '../interface/rx_storage.dart'; import '../interface/storage.dart'; +import '../logger/event.dart'; import '../logger/logger.dart'; +import '../model/error.dart'; import '../model/key_and_value.dart'; -import '../stream_extensions/map_not_null.dart'; -import '../stream_extensions/single_subscription.dart'; + +// TODO(assert) +// ignore_for_file: unnecessary_null_comparison /// Default [RxStorage] implementation. -class RealRxStorage> - implements RxStorage { +class RealRxStorage> implements RxStorage { + static const _initialKeyValue = + KeyAndValue('rx_storage', 'Petrus Nguyen Thai Hoc '); + /// Trigger subject - final _keyValuesSubject = PublishSubject>(); + final _keyValuesSubject = PublishSubject>(); final _disposeMemo = AsyncMemoizer(); + late final _bag = + DisposeBag(const [], 'RealRxStorage#${_shortHash(this)}'); - var _isDisposed = false; - - /// Logger subscription. Nullable - StreamSubscription> _subscription; + /// Logger controller. Nullable + StreamController>? _loggerEventController; /// Nullable. - S _storage; + S? _storage; - /// Nullable. - Future _storageFuture; + late Future _storageFuture; /// Nullable - final Logger _logger; - - /// Nullable - final void Function() _onDispose; + final void Function()? _onDispose; /// Construct a [RealRxStorage]. RealRxStorage( FutureOr storageOrFuture, [ - this._logger, + final Logger? logger, this._onDispose, ]) : assert(storageOrFuture != null) { if (storageOrFuture is Future) { - _storageFuture = storageOrFuture.then((value) => _storage = value); + _storageFuture = storageOrFuture.then((value) { + assert(_storage is! RxStorage); + return _storage = value; + }); } else { - _storageFuture = null; - _storage = storageOrFuture as S; - } - - if (_logger == null) { - return; + _storage = storageOrFuture; + assert(_storage is! RxStorage); } - _subscription = _keyValuesSubject.listen((map) { - final pairs = [ - for (final entry in map.entries) - KeyAndValue( - entry.key, - entry.value, - ), - ]; - _logger.keysChanged(UnmodifiableListView(pairs)); - }); + _keyValuesSubject.disposedBy(_bag); + logger?.let(_setupLogger); } // // Internal // + void _setupLogger(Logger logger) { + _loggerEventController = StreamController(sync: true) + ..disposedBy(_bag) + ..stream.listen(logger.log).disposedBy(_bag); + + _keyValuesSubject + .map>( + (map) => KeysChangedEvent(_mapToList(map))) + .listen(_loggerEventController!.add) + .disposedBy(_bag); + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + bool get _isLogEnabled => _loggerEventController != null; + + /// Crash if [_loggerEventController] is null. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void _publishLog(LoggerEvent event) { + assert(_debugAssertNotDisposed()); + + try { + _loggerEventController!.add(event); + } on StateError { + assert(_debugAssertNotDisposed()); + } + } + bool _debugAssertNotDisposed() { assert(() { - if (_isDisposed && _disposeMemo.hasRun) { + if (_bag.isDisposed && _disposeMemo.hasRun) { throw StateError('A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.'); } @@ -81,9 +103,49 @@ class RealRxStorage> return true; } + /// Calling [block] with [S] as argument. + Future _useStorage(Future Function(S) block) => + _storage?.let(block) ?? _storageFuture.then(block); + + // + // Protected + // + + /// Calling [block] with [S] as argument. + Future useStorageWithHandlers( + Future Function(S) block, + FutureOr Function(R, S) onSuccess, + FutureOr Function(RxStorageError, S) onFailure, + ) async { + assert(_debugAssertNotDisposed()); + assert(block != null); + assert(onSuccess != null); + assert(onFailure != null); + + final storage = _storage ?? await _storageFuture; + + try { + final value = await block(storage); + final futureOrVoid = onSuccess(value, storage); + if (futureOrVoid is Future) { + await futureOrVoid; + } + return value; + } catch (e, s) { + final futureOrVoid = onFailure(RxStorageError(e, s), storage); + if (futureOrVoid is Future) { + await futureOrVoid; + } + rethrow; + } + } + /// Add changed map to subject to trigger. @protected - void sendChange(Map map) { + void sendChange(Map map) { + assert(_debugAssertNotDisposed()); + assert(map != null); + try { _keyValuesSubject.add(map); } on StateError { @@ -91,153 +153,204 @@ class RealRxStorage> } } - /// Calling [block] with [S] as argument. + /// Log event. @protected - Future useStorage(Future Function(S) block) => - _storage != null ? block(_storage) : _storageFuture.then(block); + void log(LoggerEvent event) { + assert(_debugAssertNotDisposed()); + assert(event != null); + if (_isLogEnabled) { + _publishLog(event); + } + } + + // // Get and set methods (implements [Storage]) + // @override - Future containsKey(Key key, [Options options]) async { + Future containsKey(Key key, [Options? options]) async { assert(_debugAssertNotDisposed()); assert(key != null); - return useStorage((s) => s.containsKey(key, options)); + return await _useStorage((s) => s.containsKey(key, options)); } @override - Future read(Key key, Decoder decoder, [Options options]) async { + Future read(Key key, Decoder decoder, + [Options? options]) { assert(_debugAssertNotDisposed()); assert(key != null); assert(decoder != null); - final value = await useStorage((s) => s.read(key, decoder, options)); - _logger?.readValue(T, key, value); - return value; + return useStorageWithHandlers( + (s) => s.read(key, decoder, options), + (value, _) { + if (_isLogEnabled) { + _publishLog( + ReadValueSuccessEvent(KeyAndValue(key, value), T, options)); + } + }, + (error, _) { + if (_isLogEnabled) { + _publishLog(ReadValueFailureEvent(key, T, error, options)); + } + }, + ); } @override - Future> readAll([Options options]) async { + Future> readAll([Options? options]) { assert(_debugAssertNotDisposed()); - final all = await useStorage((s) => s.readAll(options)); - if (_logger != null) { - all.forEach( - (key, dynamic value) => _logger.readValue(dynamic, key, value)); - } - return all; + return useStorageWithHandlers( + (s) => s.readAll(options), + (value, _) { + if (_isLogEnabled) { + _publishLog(ReadAllSuccessEvent(_mapToList(value), options)); + } + }, + (error, _) { + if (_isLogEnabled) { + _publishLog(ReadAllFailureEvent(error, options)); + } + }, + ); } @override - Future clear([Options options]) async { + Future clear([Options? options]) async { assert(_debugAssertNotDisposed()); - final keys = (await readAll()).keys; - final result = await useStorage((s) => s.clear(options)); - - // All values are set to null - if (_logger != null) { - for (final key in keys) { - _logger.writeValue(dynamic, key, null, result); - } - } - if (result) { - final map = {for (final k in keys) k: null}; - sendChange(map); - } - - return result; + final keys = (await _useStorage((s) => s.readAll(options))).keys; + + return await useStorageWithHandlers( + (s) => s.clear(options), + (_, __) { + sendChange({for (final k in keys) k: null}); + if (_isLogEnabled) { + _publishLog(ClearSuccessEvent(options)); + } + }, + (error, _) { + if (_isLogEnabled) { + _publishLog(ClearFailureEvent(error, options)); + } + }, + ); } @override - Future remove(Key key, [Options options]) async { + Future remove(Key key, [Options? options]) { assert(_debugAssertNotDisposed()); assert(key != null); - final result = await useStorage((s) => s.remove(key, options)); - - _logger?.writeValue(dynamic, key, null, result); - if (result) { - sendChange({key: null}); - } - - return result; + return useStorageWithHandlers( + (s) => s.remove(key, options), + (_, __) { + sendChange({key: null}); + if (_isLogEnabled) { + _publishLog(RemoveSuccessEvent(key, options)); + } + }, + (error, _) { + if (_isLogEnabled) { + _publishLog(RemoveFailureEvent(key, options, error)); + } + }, + ); } @override - Future write(Key key, T value, Encoder encoder, - [Options options]) async { + Future write(Key key, T? value, Encoder encoder, + [Options? options]) { assert(_debugAssertNotDisposed()); assert(key != null); assert(encoder != null); - final result = - await useStorage((s) => s.write(key, value, encoder, options)); - - _logger?.writeValue(T, key, value, result); - if (result) { - sendChange({key: value}); - } - - return result; + return useStorageWithHandlers( + (s) => s.write(key, value, encoder, options), + (_, __) { + sendChange({key: value}); + if (_isLogEnabled) { + _publishLog(WriteSuccessEvent(KeyAndValue(key, value), T, options)); + } + }, + (error, __) { + if (_isLogEnabled) { + _publishLog( + WriteFailureEvent(KeyAndValue(key, value), T, options, error)); + } + }, + ); } + // // Get streams (implements [RxStorage]) + // @override - Stream observe(Key key, Decoder decoder, [Options options]) { + Stream observe(Key key, Decoder decoder, + [Options? options]) { assert(_debugAssertNotDisposed()); assert(key != null); final stream = _keyValuesSubject .toSingleSubscriptionStream() .mapNotNull((map) => map.containsKey(key) - ? KeyAndValue(key, map[key]) + ? KeyAndValue(key, map[key]) : null) - .startWith(null) // Dummy value to trigger initial load. - .asyncMap((entry) => entry == null - ? read(key, decoder, options) - : entry.value as FutureOr); - - if (_logger == null) { - return stream; - } + .startWith(_initialKeyValue) // Dummy value to trigger initial load. + .asyncMap( + (entry) => identical(entry, _initialKeyValue) + ? _useStorage((s) => s.read(key, decoder, options)) + : entry.value as FutureOr, + ); - return stream - .doOnData( - (value) => _logger.doOnDataStream(KeyAndValue(key, value))) - .doOnError((e, StackTrace s) => _logger.doOnErrorStream(e, s)); + return _isLogEnabled + ? stream + .doOnData((value) => + _publishLog(OnDataStreamEvent(KeyAndValue(key, value)))) + .doOnError((e, s) => _publishLog( + OnErrorStreamEvent(RxStorageError(e, s ?? StackTrace.empty)))) + : stream; } @override - Stream> observeAll([Options options]) { + Stream> observeAll([Options? options]) { assert(_debugAssertNotDisposed()); return _keyValuesSubject .toSingleSubscriptionStream() + .mapTo(null) .startWith(null) - .asyncMap((_) => readAll(options)); + .asyncMap((_) => _useStorage((s) => s.readAll(options))); } @override Future dispose() { assert(_debugAssertNotDisposed()); - return _disposeMemo.runOnce(() async { - final cancelFuture = _subscription?.cancel(); + return _disposeMemo.runOnce(_bag.dispose).then((_) => _onDispose?.call()); + } +} - if (cancelFuture == null) { - await _keyValuesSubject.close(); - } else { - await Future.wait( - [_keyValuesSubject.close(), cancelFuture], - eagerError: true, - ); - } +List> _mapToList( + Map map) { + final pairs = + map.entries.map((e) => KeyAndValue(e.key, e.value)); + return List.unmodifiable(pairs); +} - _isDisposed = true; - _onDispose?.call(); - }); - } +/// Scope function extension +extension _ScopeFunctionExtension on T { + /// Returns result from calling [f]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + R let(R Function(T) block) => block(this); } + +/// Returns a 5 character long hexadecimal string generated from +/// [Object.hashCode]'s 20 least-significant bits. +String _shortHash(Object? object) => + object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); diff --git a/lib/src/interface/rx_storage.dart b/lib/src/interface/rx_storage.dart index 92f883e..a9cee8d 100644 --- a/lib/src/interface/rx_storage.dart +++ b/lib/src/interface/rx_storage.dart @@ -5,12 +5,13 @@ import '../logger/logger.dart'; import 'storage.dart'; /// Get [Stream]s by key from persistent storage. -abstract class RxStorage implements Storage { +abstract class RxStorage + implements Storage { /// Constructs a [RxStorage] by wrapping a [Storage]. factory RxStorage( FutureOr> storageOrFuture, [ - Logger logger, - void Function() onDispose, + Logger? logger, + void Function()? onDispose, ]) => RealRxStorage>( storageOrFuture, @@ -20,11 +21,12 @@ abstract class RxStorage implements Storage { /// Return [Stream] that will emit value read from persistent storage. /// It will automatic emit value when value associated with key was changed. - Stream observe(Key key, Decoder decoder, [Options options]); + Stream observe(Key key, Decoder decoder, + [Options options]); /// Return [Stream] that will emit all values associated with key read from persistent storage. /// It will automatic emit all keys when any value was changed. - Stream> observeAll([Options options]); + Stream> observeAll([Options options]); /// Clean up resources - Closes the streams. /// This method should be called when a [RxStorage] is no longer needed. diff --git a/lib/src/interface/storage.dart b/lib/src/interface/storage.dart index f85c09b..cb8d287 100644 --- a/lib/src/interface/storage.dart +++ b/lib/src/interface/storage.dart @@ -1,30 +1,31 @@ /// Convert [T] to type that [Storage] can be persisted. /// This used in [Storage.write]. -typedef Encoder = dynamic Function(T); +typedef Encoder = Object? Function(T); /// Convert storage persisted type to [T]. /// This used in [Storage.read]. -typedef Decoder = T Function(dynamic); +typedef Decoder = T Function(Object?); /// A persistent store for simple data. Data is persisted to disk asynchronously. -abstract class Storage { +abstract class Storage { /// Returns a future complete with value true if the persistent storage /// contains the given [key]. - Future containsKey(Key key, [Options options]); + Future containsKey(Key key, [Options? options]); /// Reads a value of any type from persistent storage. - Future read(Key key, Decoder decoder, [Options options]); + Future read(Key key, Decoder decoder, + [Options? options]); /// Returns all keys in the persistent storage. - Future> readAll([Options options]); + Future> readAll([Options? options]); /// Completes with true once the storage for the app has been cleared. - Future clear([Options options]); + Future clear([Options? options]); /// Removes an entry from persistent storage. - Future remove(Key key, [Options options]); + Future remove(Key key, [Options? options]); /// Saves a [value] to persistent storage. - Future write(Key key, T value, Encoder encoder, - [Options options]); + Future write(Key key, T? value, Encoder encoder, + [Options? options]); } diff --git a/lib/src/logger/default_logger.dart b/lib/src/logger/default_logger.dart index b434d3e..bec74f5 100644 --- a/lib/src/logger/default_logger.dart +++ b/lib/src/logger/default_logger.dart @@ -1,30 +1,151 @@ -import '../model/key_and_value.dart'; +import 'event.dart'; import 'logger.dart'; /// Default Logger's implementation, simply print to the console. -class DefaultLogger implements Logger { +class DefaultLogger + implements Logger { + static const _rightArrow = '→'; + static const _leftArrow = '←'; + static const _downArrow = '↓'; + /// Construct a [DefaultLogger]. const DefaultLogger(); @override - void keysChanged(Iterable> pairs) { - print(' ↓ Key changes'); - print(pairs.map((p) => ' → $p').join('\n')); - } + void log(LoggerEvent event) { + // + // BEGIN: STREAM + // - @override - void doOnDataStream(KeyAndValue pair) => - print(' → Stream emits data: $pair'); + if (event is KeysChangedEvent) { + print(' $_downArrow Key changes'); + print(event.pairs.map((p) => ' $_rightArrow $p').join('\n')); + return; + } - @override - void doOnErrorStream(Object error, StackTrace stackTrace) => - print(' → Stream emits error: $error, $stackTrace'); + if (event is OnDataStreamEvent) { + print(' $_rightArrow Stream emits data: ${event.pair}'); + return; + } - @override - void readValue(Type type, Object key, dynamic value) => - print(" → Read value: type $type, key '$key' → $value"); + if (event is OnErrorStreamEvent) { + print(' $_rightArrow Stream emits error: ${event.error}'); + return; + } - @override - void writeValue(Type type, Object key, dynamic value, bool writeResult) => print( - " → Write value: type $type, key '$key', value $value → result $writeResult"); + // + // END: STREAM + // + + // + // BEGIN: READ + // + + if (event is ReadValueSuccessEvent) { + final key = event.pair.key; + final value = event.pair.value; + final type = event.type; + final options = event.options; + print( + " $_rightArrow Read: type=$type, key='$key'${_concatOptionsIfNotNull(options)} $_rightArrow $value"); + return; + } + + if (event is ReadValueFailureEvent) { + final key = event.key; + final type = event.type; + final options = event.options; + final error = event.error; + print( + " $_rightArrow Read: type=$type, key='$key'${_concatOptionsIfNotNull(options)} $_rightArrow $error"); + return; + } + + if (event is ReadAllSuccessEvent) { + final all = event.all; + final options = event.options; + print(' $_downArrow Read all: ${_concatOptionsIfNotNull(options, '')}'); + print(all.map((p) => ' $_rightArrow $p').join('\n')); + return; + } + + if (event is ReadAllFailureEvent) { + final options = event.options; + final error = event.error; + print( + ' $_rightArrow Read all: ${_concatOptionsIfNotNull(options, ':')} $_rightArrow $error'); + return; + } + + // + // END: READ + // + + // + // BEGIN: WRITE + // + + if (event is ClearSuccessEvent) { + final options = event.options; + print( + ' $_leftArrow Clear: ${_concatOptionsIfNotNull(options, ':')} $_rightArrow success'); + return; + } + + if (event is ClearFailureEvent) { + final options = event.options; + final error = event.error; + print( + ' $_leftArrow Clear: ${_concatOptionsIfNotNull(options, ':')} $_rightArrow $error'); + return; + } + + if (event is RemoveSuccessEvent) { + final key = event.key; + final options = event.options; + print( + " $_leftArrow Remove: key='$key'${_concatOptionsIfNotNull(options)} $_rightArrow success"); + return; + } + + if (event is RemoveFailureEvent) { + final key = event.key; + final options = event.options; + final error = event.error; + print( + " $_leftArrow Remove: key='$key'${_concatOptionsIfNotNull(options)} $_rightArrow $error"); + return; + } + + if (event is WriteSuccessEvent) { + final key = event.pair.key; + final value = event.pair.value; + final type = event.type; + final options = event.options; + print( + " $_leftArrow Write: key='$key', value=$value, type=$type${_concatOptionsIfNotNull(options)} $_rightArrow success"); + return; + } + + if (event is WriteFailureEvent) { + final key = event.pair.key; + final value = event.pair.value; + final type = event.type; + final options = event.options; + final error = event.error; + print( + " $_leftArrow Write: key='$key', value=$value, type=$type${_concatOptionsIfNotNull(options)} $_rightArrow $error"); + return; + } + + // + // END: WRITE + // + + throw Exception('Unhandled event: $event'); + } + + static String _concatOptionsIfNotNull(Object? options, + [String separator = ',']) => + options == null ? '' : '$separator options=$options'; } diff --git a/lib/src/logger/empty_logger.dart b/lib/src/logger/empty_logger.dart new file mode 100644 index 0000000..4f65cb5 --- /dev/null +++ b/lib/src/logger/empty_logger.dart @@ -0,0 +1,11 @@ +import 'event.dart'; +import 'logger.dart'; + +/// Logger's implementation with empty methods. +class EmptyLogger implements Logger { + /// Constructs a [EmptyLogger]. + const EmptyLogger(); + + @override + void log(LoggerEvent event) {} +} diff --git a/lib/src/logger/event.dart b/lib/src/logger/event.dart new file mode 100644 index 0000000..3bd49bc --- /dev/null +++ b/lib/src/logger/event.dart @@ -0,0 +1,210 @@ +import 'package:meta/meta.dart'; + +import '../model/error.dart'; +import '../model/key_and_value.dart'; + +/// Event when reading, writing storage. +@immutable +abstract class LoggerEvent {} + +// +// BEGIN: STREAM +// + +/// Key changed when mutating storage. +class KeysChangedEvent + implements LoggerEvent { + /// A list containing all changed values associated with keys. + final List> pairs; + + /// Construct a [KeysChangedEvent]. + KeysChangedEvent(this.pairs); +} + +/// Stream emits new data event. +class OnDataStreamEvent + implements LoggerEvent { + /// Changed value with key. + final KeyAndValue pair; + + /// Construct a [OnDataStreamEvent]. + OnDataStreamEvent(this.pair); +} + +/// Stream emits error event. +class OnErrorStreamEvent + implements LoggerEvent { + /// Error from upstream (eg. cast error, ...) + final RxStorageError error; + + /// Construct a [OnErrorStreamEvent]. + OnErrorStreamEvent(this.error); +} + +// +// END: STREAM +// + +// +// BEGIN: READ +// + +/// Read value successfully. +class ReadValueSuccessEvent + implements LoggerEvent { + /// Read value with key. + final KeyAndValue pair; + + /// The type of read value. + final Type type; + + /// The options. + final Options? options; + + /// Construct a [ReadValueSuccessEvent]. + ReadValueSuccessEvent(this.pair, this.type, this.options); +} + +/// Read value failed. +class ReadValueFailureEvent + implements LoggerEvent { + /// The key. + final Key key; + + /// The expected type of value. + final Type type; + + /// The error occurred when reading. + final RxStorageError error; + + /// The options. + final Options? options; + + /// Construct a [ReadValueFailureEvent]. + ReadValueFailureEvent(this.key, this.type, this.error, this.options); +} + +/// Read all values successfully. +class ReadAllSuccessEvent + implements LoggerEvent { + /// All values read from storage. + final List> all; + + /// The options. + final Options? options; + + /// Construct a [ReadAllSuccessEvent]. + ReadAllSuccessEvent(this.all, this.options); +} + +/// Read all values failed. +class ReadAllFailureEvent + implements LoggerEvent { + /// The error occurred when reading. + final RxStorageError error; + + /// The options. + final Options? options; + + /// Construct a [ReadAllFailureEvent]. + ReadAllFailureEvent(this.error, this.options); +} + +// +// END: READ +// + +// +// BEGIN: WRITE +// + +/// Clear storage successfully. +class ClearSuccessEvent + implements LoggerEvent { + /// The options. + final Options? options; + + /// Construct a [ClearSuccessEvent]. + ClearSuccessEvent(this.options); +} + +/// Clear storage failed. +class ClearFailureEvent + implements LoggerEvent { + /// The error occurred while clearing. + final RxStorageError error; + + /// The options. + final Options? options; + + /// Construct a [ClearFailureEvent]. + ClearFailureEvent(this.error, this.options); +} + +/// Remove successfully. +class RemoveSuccessEvent + implements LoggerEvent { + /// The key. + final Key key; + + /// The options. + final Options? options; + + /// Construct a [RemoveSuccessEvent]. + RemoveSuccessEvent(this.key, this.options); +} + +/// Remove failed. +class RemoveFailureEvent + implements LoggerEvent { + /// The key. + final Key key; + + /// The options. + final Options? options; + + /// The error occurred when removing. + final RxStorageError error; + + /// Construct a [RemoveFailureEvent]. + RemoveFailureEvent(this.key, this.options, this.error); +} + +/// Write successfully. +class WriteSuccessEvent + implements LoggerEvent { + /// The key and value. + final KeyAndValue pair; + + /// The type of value. + final Type type; + + /// The options. + final Options? options; + + /// Construct a [WriteSuccessEvent]. + WriteSuccessEvent(this.pair, this.type, this.options); +} + +/// Write failed. +class WriteFailureEvent + implements LoggerEvent { + /// The key and value. + final KeyAndValue pair; + + /// The type of value. + final Type type; + + /// The options. + final Options? options; + + /// The error occurred when removing. + final RxStorageError error; + + /// Construct a [WriteFailureEvent]. + WriteFailureEvent(this.pair, this.type, this.options, this.error); +} + +// +// END: WRITE +// diff --git a/lib/src/logger/logger.dart b/lib/src/logger/logger.dart index 5cea595..c17cabc 100644 --- a/lib/src/logger/logger.dart +++ b/lib/src/logger/logger.dart @@ -1,20 +1,7 @@ -import '../interface/storage.dart'; -import '../model/key_and_value.dart'; +import 'event.dart'; /// Log messages about operations (such as read, write, value change) and stream events. -abstract class Logger { - /// Called when values have changed. - void keysChanged(Iterable> pairs); - - /// Called when the stream emits an item. - void doOnDataStream(KeyAndValue pair); - - /// Called when the stream emits an error. - void doOnErrorStream(Object error, StackTrace stackTrace); - - /// Called when reading value from [Storage]. - void readValue(Type type, Object key, dynamic value); - - /// Called when writing value to [Storage]. - void writeValue(Type type, Object key, dynamic value, bool writeResult); +abstract class Logger { + /// Logs event. + void log(LoggerEvent event); } diff --git a/lib/src/logger/logger_adapter.dart b/lib/src/logger/logger_adapter.dart deleted file mode 100644 index 5c5fbdc..0000000 --- a/lib/src/logger/logger_adapter.dart +++ /dev/null @@ -1,23 +0,0 @@ -import '../model/key_and_value.dart'; -import 'logger.dart'; - -/// Logger's implementation with empty methods. -class LoggerAdapter implements Logger { - /// Constructs a [LoggerAdapter]. - const LoggerAdapter(); - - @override - void doOnDataStream(KeyAndValue pair) {} - - @override - void doOnErrorStream(Object error, StackTrace stackTrace) {} - - @override - void keysChanged(Iterable> pairs) {} - - @override - void readValue(Type type, Object key, dynamic value) {} - - @override - void writeValue(Type type, Object key, dynamic value, bool writeResult) {} -} diff --git a/lib/src/model/error.dart b/lib/src/model/error.dart new file mode 100644 index 0000000..aff37fc --- /dev/null +++ b/lib/src/model/error.dart @@ -0,0 +1,27 @@ +/// An Object which acts as a tuple containing both an error and the +/// corresponding stack trace. +class RxStorageError { + /// A reference to the wrapped error object. + final Object error; + + /// A reference to the wrapped [StackTrace] + final StackTrace stackTrace; + + /// Constructs an object containing both an [error] and the + /// corresponding [stackTrace]. + const RxStorageError(this.error, this.stackTrace); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RxStorageError && + runtimeType == other.runtimeType && + error == other.error && + stackTrace == other.stackTrace; + + @override + int get hashCode => error.hashCode ^ stackTrace.hashCode; + + @override + String toString() => '$error, $stackTrace'; +} diff --git a/lib/src/model/key_and_value.dart b/lib/src/model/key_and_value.dart index d2ee35f..da94b23 100644 --- a/lib/src/model/key_and_value.dart +++ b/lib/src/model/key_and_value.dart @@ -1,5 +1,5 @@ /// Pair of [key] and [value]. -class KeyAndValue { +class KeyAndValue { /// The key of the [KeyAndValue]. final K key; diff --git a/lib/src/stream_extensions/map_not_null.dart b/lib/src/stream_extensions/map_not_null.dart deleted file mode 100644 index bf44e13..0000000 --- a/lib/src/stream_extensions/map_not_null.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:async'; - -/// Transforms each element of this stream into a new stream event, and reject null. -/// ### Example -/// -/// Stream.fromIterable([1, 'two', 3, 'four']) -/// .mapNotNull((i) => i is int ? i * 2 : null) -/// .listen(print); // prints 2, 6 -/// -/// #### as opposed to: -/// -/// Stream.fromIterable([1, 'two', 3, 'four']) -/// .map((i) => i is int ? i * 2 : null) -/// .where((i) => i != null) -/// .listen(print); // prints 2, 6 -extension MapNotNullStreamExtension on Stream { - /// Transforms each element of this stream into a new stream event, and reject null. - Stream mapNotNull(R Function(T) mapper) { - assert(mapper != null); - - StreamController controller; - StreamSubscription subscription; - - void onListen() { - subscription = listen( - (data) { - R mappedValue; - - try { - mappedValue = mapper(data); - } catch (e, s) { - controller.addError(e, s); - return; - } - - if (mappedValue != null) { - controller.add(mappedValue); - } - }, - onError: controller.addError, - onDone: controller.close, - ); - } - - Future onCancel() => subscription.cancel(); - - if (isBroadcast) { - controller = StreamController.broadcast( - sync: true, - onListen: onListen, - onCancel: onCancel, - ); - } else { - controller = StreamController( - sync: true, - onListen: onListen, - onPause: () => subscription.pause(), - onResume: () => subscription.resume(), - onCancel: onCancel, - ); - } - - return controller.stream; - } -} diff --git a/lib/src/stream_extensions/single_subscription.dart b/lib/src/stream_extensions/single_subscription.dart deleted file mode 100644 index f4382af..0000000 --- a/lib/src/stream_extensions/single_subscription.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:async'; - -/// Converts a broadcast Stream into a single-subscription stream. -extension ToSingleSubscriptionStreamExtension on Stream { - /// Converts a broadcast Stream into a single-subscription stream. - Stream toSingleSubscriptionStream() { - assert(isBroadcast == true); - final controller = StreamController(sync: true); - - StreamSubscription subscription; - controller.onListen = () { - subscription = listen( - controller.add, - onError: controller.addError, - onDone: controller.close, - ); - }; - controller.onCancel = () => subscription.cancel(); - - return controller.stream; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 1244a8d..97b7c73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,19 +1,20 @@ name: rx_storage description: Reactive storage for Dart/Flutter. RxDart Storage for Dart/Flutter. -version: 0.0.2 +version: 1.0.0-nullsafety.0 author: Petrus Nguyen Thai Hoc homepage: https://github.com/Flutter-Dart-Open-Source/rx_storage.git repository: https://github.com/Flutter-Dart-Open-Source/rx_storage.git issue_tracker: https://github.com/Flutter-Dart-Open-Source/rx_storage/issues environment: - sdk: '>=2.7.0 <3.0.0' + sdk: '>=2.12.0-0 <3.0.0' dependencies: - rxdart: '>=0.25.0 <0.26.0' - meta: ^1.1.8 + rxdart_ext: ^0.0.1-nullsafety.7 + disposebag: ^1.5.0-nullsafety.1 + meta: ^1.3.0 dev_dependencies: - pedantic: ^1.9.0 - test: ^1.15.3 - collection: ^1.14.13 + pedantic: ^1.10.0 + test: ^1.16.2 + collection: ^1.15.0 diff --git a/test/fake_storage.dart b/test/fake_storage.dart index e78d089..f06d4d4 100644 --- a/test/fake_storage.dart +++ b/test/fake_storage.dart @@ -1,30 +1,59 @@ import 'dart:async'; import 'package:rx_storage/rx_storage.dart'; -import 'package:rx_storage/src/interface/storage.dart'; import 'utils/synchronous_future.dart'; Future _wrap(T value) => SynchronousFuture(value); abstract class StringKeyStorage extends Storage { - Future reload(); + Future> reload(); } abstract class StringKeyRxStorage extends StringKeyStorage implements RxStorage {} +class FakeDefaultLogger extends DefaultLogger { + const FakeDefaultLogger(); + + @override + void log(LoggerEvent event) { + if (event is ReloadSuccessEvent) { + print('ReloadSuccessEvent ${event.map}'); + return; + } + if (event is ReloadFailureEvent) { + print('ReloadFailureEvent ${event.error}'); + return; + } + + super.log(event); + } +} + +class ReloadSuccessEvent implements LoggerEvent { + final Map map; + + ReloadSuccessEvent(this.map); +} + +class ReloadFailureEvent implements LoggerEvent { + final RxStorageError error; + + ReloadFailureEvent(this.error); +} + class FakeStorage implements StringKeyStorage { - Map _map; - Map _pendingMap; + Map _map; + Map? _pendingMap; - FakeStorage(Map map) : _map = Map.of(map); + FakeStorage(Map map) : _map = Map.of(map); - set map(Map map) => - _pendingMap = Map.of(map); + set map(Map map) => + _pendingMap = Map.of(map); - Future _setValue(String key, dynamic value) { - if (value is List) { + Future _setValue(String key, Object? value) { + if (value is List?) { _map[key] = value?.toList(); } else { _map[key] = value; @@ -32,8 +61,8 @@ class FakeStorage implements StringKeyStorage { return _wrap(true); } - Future _getValue(String key) { - final value = _map[key] as T; + Future _getValue(String key) { + final value = _map[key] as T?; return value is List ? _wrap(value.toList() as T) : _wrap(value); } @@ -42,7 +71,7 @@ class FakeStorage implements StringKeyStorage { // @override - Future clear([void _]) { + Future clear([void _]) { _map.clear(); return _wrap(true); } @@ -52,36 +81,41 @@ class FakeStorage implements StringKeyStorage { _wrap(_map.containsKey(key)); @override - Future write(String key, T value, Encoder encoder, [void _]) => + Future write( + String key, T? value, Encoder encoder, + [void _]) => _setValue(key, encoder(value)); @override - Future reload() { + Future> reload() { if (_pendingMap != null) { - _map = _pendingMap; + _map = _pendingMap!; _pendingMap = null; + return _wrap(_map); + } else { + throw StateError('Cannot reload'); } - return _wrap(null); } @override - Future remove(String key, [void _]) => _setValue(key, null); + Future remove(String key, [void _]) => _setValue(key, null); @override - Future read(String key, Decoder decoder, [void _]) => - _getValue(key).then(decoder); + Future read(String key, Decoder decoder, + [void _]) => + _getValue(key).then(decoder); @override - Future> readAll([void _]) => - _wrap({..._map}); + Future> readAll([void _]) => + _wrap({..._map}); } class FakeRxStorage extends RealRxStorage - implements StringKeyRxStorage { + implements StringKeyRxStorage, StringKeyStorage { FakeRxStorage( FutureOr storageOrFuture, [ - Logger logger, - void Function() onDispose, + Logger? logger, + void Function()? onDispose, ]) : super( storageOrFuture, logger, @@ -89,8 +123,29 @@ class FakeRxStorage extends RealRxStorage ); @override - Future reload() async { - await useStorage((s) => s.reload()); - sendChange(await readAll()); + Future> reload() async { + final handler = (Object? _, Object? __) => null; + final before = + await useStorageWithHandlers((s) => s.readAll(), handler, handler); + + return useStorageWithHandlers( + (s) => s.reload(), + (value, s) { + sendChange(computeMap(before, value)); + log(ReloadSuccessEvent(value)); + }, + (error, _) => log(ReloadFailureEvent(error)), + ); } } + +Map computeMap( + Map before, + Map after, +) { + final deletedKeys = before.keys.toSet().difference(after.keys.toSet()); + return { + ...after, + for (final k in deletedKeys) k: null, + }; +} diff --git a/test/logger/default_logger_test.dart b/test/logger/default_logger_test.dart index f60581f..dd42971 100644 --- a/test/logger/default_logger_test.dart +++ b/test/logger/default_logger_test.dart @@ -1,103 +1,53 @@ -import 'dart:async'; - -import 'package:rx_storage/src/logger/default_logger.dart'; -import 'package:rx_storage/src/model/key_and_value.dart'; +import 'package:rx_storage/rx_storage.dart'; import 'package:test/test.dart'; void main() { - final logs = []; - - dynamic Function() overridePrint(dynamic Function() testFn) { - return () { - final spec = ZoneSpecification(print: (_, __, ___, String line) { - // Add to log instead of printing to stdout - logs.add(line); - }); - return Zone.current.fork(specification: spec).run(testFn); - }; - } - - group('DefaultLogger', () { - final logger = DefaultLogger(); - - setUp(() => logs.clear()); - - test( - 'keysChanged', - overridePrint(() { - const pairs = [ - KeyAndValue('key1', 'value1'), - KeyAndValue('key2', 2), - ]; - logger.keysChanged(pairs); - expect( - logs, - [ - ' ↓ Key changes', - " → { 'key1': value1 }" '\n' " → { 'key2': 2 }", - ], - ); - }), - ); - - test( - 'doOnDataStream', - overridePrint(() { - const keyAndValue = KeyAndValue('key1', 'value1'); - logger.doOnDataStream(keyAndValue); - - expect( - logs, - [" → Stream emits data: { 'key1': value1 }"], - ); - }), - ); - - test( - 'doOnErrorStream', - overridePrint(() { - final stackTrace = StackTrace.current; - final exception = Exception(); - logger.doOnErrorStream(exception, stackTrace); - - expect( - logs, - [' → Stream emits error: $exception, $stackTrace'], - ); - }), - ); - - test( - 'readValue', - overridePrint(() { - const type = String; - const key = 'key'; - const value = 'value'; - logger.readValue(type, key, value); - - expect( - logs, - [" → Read value: type String, key 'key' → value"], - ); - }), - ); - - test( - 'writeValue', - overridePrint(() { - const type = String; - const key = 'key'; - const value = 'value'; - const writeResult = true; - logger.writeValue(type, key, value, writeResult); - - expect( - logs, - [ - " → Write value: type String, key 'key', value value → result true" - ], - ); - }), - ); + group('DefaultLogger', () { + final logger = DefaultLogger(); + + test('KeysChangedEvent', () { + const pairs = [ + KeyAndValue('key1', 'value1'), + KeyAndValue('key2', 2), + ]; + logger.log(KeysChangedEvent(pairs)); + prints([ + ' ↓ Key changes', + " → { 'key1': value1 }" '\n' " → { 'key2': 2 }", + ].join('\n')); + }); + + test('OnDataStreamEvent', () { + const pair = KeyAndValue('key1', 'value1'); + logger.log(OnDataStreamEvent(pair)); + + prints(" → Stream emits data: { 'key1': value1 }"); + }); + + test('OnErrorStreamEvent', () { + final stackTrace = StackTrace.current; + final exception = Exception(); + logger.log(OnErrorStreamEvent(RxStorageError(exception, stackTrace))); + + prints(' → Stream emits error: $exception, $stackTrace'); + }); + + test('ReadValueSuccessEvent', () { + const type = String; + const key = 'key'; + const value = 'value'; + logger.log(ReadValueSuccessEvent(KeyAndValue(key, value), type, null)); + + prints(" → Read: type=String, key='key' → value"); + }); + + test('WriteSuccessEvent', () { + const type = String; + const key = 'key'; + const value = 'value'; + logger.log(WriteSuccessEvent(KeyAndValue(key, value), type, null)); + + prints(" ← Write: key='key', value=value, type=String → success"); + }); }); } diff --git a/test/logger/logger_adapter_test.dart b/test/logger/logger_adapter_test.dart index bb9d30e..df5f5ac 100644 --- a/test/logger/logger_adapter_test.dart +++ b/test/logger/logger_adapter_test.dart @@ -1,26 +1,13 @@ -import 'package:rx_storage/src/logger/logger_adapter.dart'; -import 'package:rx_storage/src/model/key_and_value.dart'; +import 'package:rx_storage/rx_storage.dart'; import 'package:test/test.dart'; void main() { - group('LoggerAdapter', () { + group('EmptyLogger', () { test('Works', () { - final logger = LoggerAdapter(); + final logger = EmptyLogger(); const keyAndValue = KeyAndValue('key', 'value'); - logger.keysChanged([keyAndValue]); - logger.doOnDataStream(keyAndValue); - logger.doOnErrorStream(Exception(), StackTrace.current); - logger.writeValue( - keyAndValue.value.runtimeType, - keyAndValue.key, - keyAndValue.value, - true, - ); - logger.readValue( - keyAndValue.value.runtimeType, - keyAndValue.key, - keyAndValue.value, - ); + + logger.log(OnDataStreamEvent(keyAndValue)); }); }); } diff --git a/test/perf.dart b/test/perf.dart index 32e373c..8dcb28a 100644 --- a/test/perf.dart +++ b/test/perf.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:rx_storage/rx_storage.dart'; - import 'fake_storage.dart'; import 'utils/compat.dart'; -void main() async { +Future main() async { const kTestValues = { 'String': 'hello world', 'bool': true, @@ -14,58 +12,61 @@ void main() async { 'List': ['foo', 'bar'], }; - // - // const kTestValues2 = { - // 'String': 'goodbye world', - // 'bool': false, - // 'int': 1337, - // 'double': 2.71828, - // 'List': ['baz', 'quox'], - // }; - final storage = FakeStorage(kTestValues); - final rxStorage = RxStorage( + final rxStorage = FakeRxStorage( Future.delayed( const Duration(milliseconds: 100), () => storage, ), - const DefaultLogger(), + null, ); + rxStorage + .getStringStream('String') + .listen((v) => print('>>>>>>>>>>>>>>> $v')); + final stopwatch = Stopwatch(); final list = kTestValues.keys.toList(); + final max = 100000; + + // wake up + for (var i = 0; i < max / 2; i++) { + await rxStorage.get(list[i % list.length]); + } stopwatch ..reset() ..start(); - print('Start...'); - for (var i = 0; i < 10000; i++) { + print('>>> Start...'); + + for (var i = 0; i < max; i++) { await rxStorage.get(list[i % list.length]); } - print('End...'); stopwatch.stop(); - print(stopwatch.elapsedMilliseconds); + print('<<< End... ${stopwatch.elapsedMilliseconds}'); + print('-------------------------------------------'); // // // + await Future.delayed(const Duration(seconds: 2)); + final completer = Completer.sync(); stopwatch ..reset() ..start(); - print('Start 2...'); - rxStorage.getStringStream('key').listen((event) { - if (event == 10000.toString()) { - print('End 2...'); + print('>>> Start...'); + rxStorage.getStringStream('key').listen((event) { + if (event == max.toString()) { stopwatch.stop(); - print(stopwatch.elapsedMilliseconds); + print('<<< End... ${stopwatch.elapsedMilliseconds}'); completer.complete(); } }); - for (var i = 0; i <= 10000; i++) { + for (var i = 0; i <= max; i++) { await rxStorage.setString('key', i.toString()); } diff --git a/test/rx_storage_test.dart b/test/rx_storage_test.dart index 35c84fa..072d706 100644 --- a/test/rx_storage_test.dart +++ b/test/rx_storage_test.dart @@ -1,13 +1,13 @@ import 'logger/default_logger_test.dart' as default_logger_test; import 'logger/logger_adapter_test.dart' as logger_adapter_test; import 'model/key_and_value_test.dart' as key_and_value_test; +import 'perf.dart' as perf; import 'storage/storage_test.dart' as storage_test; import 'storage/streams_test.dart' as streams_test; -import 'stream_extensions/map_not_null_test.dart' as map_not_null_test; -import 'stream_extensions/to_single_subscription_stream_test.dart' - as to_single_subscription_stream_test; -void main() { +void main() async { + await perf.main(); + // logger tests default_logger_test.main(); logger_adapter_test.main(); @@ -18,8 +18,4 @@ void main() { // storage tests storage_test.main(); streams_test.main(); - - // stream extensions tests - map_not_null_test.main(); - to_single_subscription_stream_test.main(); } diff --git a/test/storage/storage_test.dart b/test/storage/storage_test.dart index 43ca1dc..eb6d96a 100644 --- a/test/storage/storage_test.dart +++ b/test/storage/storage_test.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:rx_storage/rx_storage.dart'; import 'package:test/test.dart'; import '../fake_storage.dart'; @@ -31,12 +30,12 @@ void main() { 'User': jsonEncode(user2), }; - FakeStorage storage; - FakeRxStorage rxStorage; + late FakeStorage storage; + late FakeRxStorage rxStorage; setUp(() { storage = FakeStorage(kTestValues); - rxStorage = FakeRxStorage(storage, const DefaultLogger()); + rxStorage = FakeRxStorage(storage, const FakeDefaultLogger()); }); tearDown(() async { @@ -55,10 +54,12 @@ void main() { expect(await rxStorage.getDouble('double'), kTestValues['double']); expect(await rxStorage.getStringList('List'), kTestValues['List']); expect(await rxStorage.readUser(), user1); + + expect(await rxStorage.readAll(), kTestValues); }); test('writing', () async { - await Future.wait(>[ + await Future.wait([ rxStorage.setString('String', kTestValues2['String'] as String), rxStorage.setBool('bool', kTestValues2['bool'] as bool), rxStorage.setInt('int', kTestValues2['int'] as int), @@ -123,7 +124,7 @@ void main() { final cachedList = await rxStorage.getStringList('myList'); expect(cachedList, []); - cachedList.add('foobar2'); + cachedList!.add('foobar2'); expect(await rxStorage.getStringList('myList'), []); }); diff --git a/test/storage/streams_test.dart b/test/storage/streams_test.dart index be6d31f..7b3ae52 100644 --- a/test/storage/streams_test.dart +++ b/test/storage/streams_test.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:rx_storage/rx_storage.dart'; import 'package:test/test.dart'; import '../fake_storage.dart'; @@ -22,15 +21,15 @@ void main() { 'User': jsonEncode(user1), }; - FakeStorage fakeStorage; - FakeRxStorage rxStorage; + late FakeStorage fakeStorage; + late FakeRxStorage rxStorage; setUp(() { fakeStorage = FakeStorage(kTestValues); rxStorage = FakeRxStorage( fakeStorage, - const DefaultLogger(), + const FakeDefaultLogger(), ); }); @@ -237,7 +236,7 @@ void main() { test('Does not emit anything after disposed', () async { final stream = rxStorage.getStringListStream('List'); - const expected = [ + const expected = [ anything, ['before', 'dispose', '1'], ['before', 'dispose', '2'], diff --git a/test/stream_extensions/map_not_null_test.dart b/test/stream_extensions/map_not_null_test.dart deleted file mode 100644 index c15bf1f..0000000 --- a/test/stream_extensions/map_not_null_test.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:async'; - -import 'package:rx_storage/src/stream_extensions/map_not_null.dart'; -import 'package:test/test.dart'; - -void main() { - group('Rx.mapNotNull', () { - test('Rx.mapNotNull', () async { - // 0-----1-----2-----3-----...-----8-----9-----| - // 1-----null--3-----null--...-----9-----null--| - // 1--3--5--7--9--| - final stream = - Stream.periodic(const Duration(milliseconds: 100), (i) => i) - .take(10) - .mapNotNull((i) => i.isOdd ? null : i + 1); - await expectLater( - stream, - emitsInOrder([1, 3, 5, 7, 9, emitsDone]), - ); - }); - - test('Rx.mapNotNull.shouldThrowA', () { - expect( - () => Stream.value(42).mapNotNull(null), - throwsA(isA()), - ); - }); - - test('Rx.mapNotNull.shouldThrowB', () async { - final stream = Stream.error(Exception()).mapNotNull((_) => true); - await expectLater( - stream, - emitsError(isA()), - ); - }); - - test('Rx.mapNotNull.shouldThrowC', () async { - final stream = Stream.fromIterable([1, 2, 3, 4]).mapNotNull((i) { - if (i == 3) { - throw Exception(); - } else { - return i; - } - }); - expect( - stream, - emitsInOrder([ - 1, - 2, - emitsError(isA()), - 4, - emitsDone, - ]), - ); - }); - - test('Rx.mapNotNull.asBroadcastStream', () { - final stream = Stream.fromIterable([2, 3, 4, 5, 6]) - .mapNotNull((i) => null) - .asBroadcastStream(); - - // listen twice on same stream - stream.listen(null); - stream.listen(null); - - // code should reach here - expect(true, true); - }); - - test('Rx.mapNotNull.pause.resume', () async { - StreamSubscription subscription; - - subscription = - Stream.fromIterable([2, 3, 4, 5, 6]).mapNotNull((i) => i).listen( - expectAsync1( - (data) { - expect(data, 2); - subscription.cancel(); - }, - ), - ); - - subscription.pause(); - subscription.resume(); - }); - - test('Rx.mapNotNull.broadcast', () { - final streamController = StreamController.broadcast(); - final stream = streamController.stream.mapNotNull((i) => i); - - expect(stream.isBroadcast, isTrue); - stream.listen(null); - stream.listen(null); - - expect(true, true); - }); - - test('Rx.mapNotNull.not.broadcast', () { - final streamController = StreamController(); - final stream = streamController.stream.mapNotNull((i) => i); - - expect(stream.isBroadcast, isFalse); - stream.listen(null); - expect(() => stream.listen(null), throwsStateError); - - streamController.add(1); - }); - }); -} diff --git a/test/stream_extensions/to_single_subscription_stream_test.dart b/test/stream_extensions/to_single_subscription_stream_test.dart deleted file mode 100644 index b06add0..0000000 --- a/test/stream_extensions/to_single_subscription_stream_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:async'; - -import 'package:rx_storage/src/stream_extensions/single_subscription.dart'; -import 'package:test/test.dart'; - -void main() { - group('Stream.toSingleSubscriptionStream', () { - test('Stream.toSingleSubscriptionStream', () { - final streamController = StreamController.broadcast(); - final singleSubscriptionStream = - streamController.stream.toSingleSubscriptionStream(); - - expect(singleSubscriptionStream.isBroadcast, isFalse); - singleSubscriptionStream.listen(null); - expect(() => singleSubscriptionStream.listen(null), throwsStateError); - }); - - test('Emitting values since listening', () { - final streamController = StreamController.broadcast(); - final singleSubscriptionStream = - streamController.stream.toSingleSubscriptionStream(); - - expect( - singleSubscriptionStream, - emitsInOrder([1, 2, 3, emitsDone]), - ); - - streamController.add(1); - streamController.add(2); - streamController.add(3); - streamController.close(); - }); - - test('Assert isBroadcast', () { - Stream.empty().toSingleSubscriptionStream(); - - expect( - () => Stream.value(1).toSingleSubscriptionStream(), - throwsA(isA()), - ); - }); - }); -} diff --git a/test/utils/compat.dart b/test/utils/compat.dart index e7fa013..c6247f6 100644 --- a/test/utils/compat.dart +++ b/test/utils/compat.dart @@ -5,36 +5,37 @@ T _identity(T t) => t; extension RxCompatExtensions on RxStorage { /// Return [Stream] that will emit value read from persistent storage. /// It will automatic emit value when value associated with key was changed. - Stream getStream(String key) => observe(key, _identity); + Stream getStream(String key) => observe(key, _identity); /// Return [Stream] that will emit value read from persistent storage. /// It will automatic emit value when value associated with [key] was changed. /// This stream will emit an error if it's not a bool. - Stream getBoolStream(String key) => - observe(key, (dynamic s) => s as bool); + Stream getBoolStream(String key) => + observe(key, (Object? s) => s as bool?); /// Return [Stream] that will emit value read from persistent storage. /// It will automatic emit value when value associated with [key] was changed. /// This stream will emit an error if it's not a double. - Stream getDoubleStream(String key) => - observe(key, (dynamic s) => s as double); + Stream getDoubleStream(String key) => + observe(key, (Object? s) => s as double?); /// Return [Stream] that will emit value read from persistent storage. /// It will automatic emit value when value associated with [key] was changed. /// This stream will emit an error if it's not a int. - Stream getIntStream(String key) => observe(key, (dynamic s) => s as int); + Stream getIntStream(String key) => + observe(key, (Object? s) => s as int?); /// Return [Stream] that will emit value read from persistent storage. /// It will automatic emit value when value associated with [key] was changed. /// This stream will emit an error if it's not a String. - Stream getStringStream(String key) => - observe(key, (dynamic s) => s as String); + Stream getStringStream(String key) => + observe(key, (Object? s) => s as String?); /// Return [Stream] that will emit value read from persistent storage. /// It will automatic emit value when value associated with [key] was changed. /// This stream will emit an error if it's not a string set. - Stream> getStringListStream(String key) => - observe(key, (dynamic s) => s as List); + Stream?> getStringListStream(String key) => + observe(key, (Object? s) => s as List?); /// Return [Stream] that will emit all keys read from persistent storage. /// It will automatic emit all keys when any value was changed. @@ -44,19 +45,20 @@ extension RxCompatExtensions on RxStorage { extension CompatExtensions on Storage { /// Reads a value of any type from persistent storage. - Future get(String key) => read(key, _identity); + Future get(String key) => read(key, _identity); /// Reads a value from persistent storage, return a future that completes /// with an error if it's not a bool. - Future getBool(String key) => read(key, (dynamic s) => s as bool); + Future getBool(String key) => read(key, (Object? s) => s as bool?); /// Reads a value from persistent storage, return a future that completes /// with an error if it's not a double. - Future getDouble(String key) => read(key, (dynamic s) => s as double); + Future getDouble(String key) => + read(key, (Object? s) => s as double?); /// Reads a value from persistent storage, return a future that completes /// with an error if it's not a int. - Future getInt(String key) => read(key, (dynamic s) => s as int); + Future getInt(String key) => read(key, (Object? s) => s as int?); /// Returns all keys in the persistent storage. Future> getKeys() => @@ -64,40 +66,41 @@ extension CompatExtensions on Storage { /// Reads a value from persistent storage, return a future that completes /// with an error if it's not a String. - Future getString(String key) => read(key, (dynamic s) => s as String); + Future getString(String key) => + read(key, (Object? s) => s as String?); /// Reads a value from persistent storage, return a future that completes /// with an error if it's not a string set. - Future> getStringList(String key) => - read(key, (dynamic s) => s as List); + Future?> getStringList(String key) => + read(key, (Object? s) => s as List?); /// Saves a boolean [value] to persistent storage in the background. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setBool(String key, bool value) => write(key, value, _identity); + Future setBool(String key, bool? value) => write(key, value, _identity); /// Saves a double [value] to persistent storage in the background. /// /// Android doesn't support storing doubles, so it will be stored as a float. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setDouble(String key, double value) => + Future setDouble(String key, double? value) => write(key, value, _identity); /// Saves an integer [value] to persistent storage in the background. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setInt(String key, int value) => write(key, value, _identity); + Future setInt(String key, int? value) => write(key, value, _identity); /// Saves a string [value] to persistent storage in the background. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setString(String key, String value) => + Future setString(String key, String? value) => write(key, value, _identity); /// Saves a list of strings [value] to persistent storage in the background. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setStringList(String key, List value) => + Future setStringList(String key, List? value) => write(key, value, _identity); } diff --git a/test/utils/synchronous_future.dart b/test/utils/synchronous_future.dart index c915186..b1f97d2 100644 --- a/test/utils/synchronous_future.dart +++ b/test/utils/synchronous_future.dart @@ -34,11 +34,12 @@ class SynchronousFuture implements Future { } @override - Future catchError(Function onError, {bool Function(Object error) test}) => + Future catchError(Function onError, {bool Function(Object error)? test}) => Completer().future; @override - Future then(FutureOr Function(T value) onValue, {Function onError}) { + Future then(FutureOr Function(T value) onValue, + {Function? onError}) { final dynamic result = onValue(_value); if (result is Future) { return result; @@ -47,7 +48,7 @@ class SynchronousFuture implements Future { } @override - Future timeout(Duration timeLimit, {FutureOr Function() onTimeout}) { + Future timeout(Duration timeLimit, {FutureOr Function()? onTimeout}) { return Future.value(_value).timeout(timeLimit, onTimeout: onTimeout); } diff --git a/test/utils/user.dart b/test/utils/user.dart index cb3e0c8..e61ad66 100644 --- a/test/utils/user.dart +++ b/test/utils/user.dart @@ -8,15 +8,15 @@ class User { const User(this.id, this.name); - factory User.fromJson(Map map) { + factory User.fromJson(Map map) { return User( map['id'] as int, map['name'] as String, ); } - Map toJson() { - return { + Map toJson() { + return { 'id': id, 'name': name, }; @@ -38,19 +38,19 @@ class User { } extension RxStoreageExtensionsForUser on RxStorage { - Future readUser() => read('User', _toUser); + Future readUser() => read('User', _toUser); - Future writeUser(User user) => write('User', user, _toString); + Future writeUser(User? user) => write('User', user, _toString); - Stream observeUser() => observe('User', _toUser); + Stream observeUser() => observe('User', _toUser); } -User _toUser(dynamic s) { +User? _toUser(Object? s) { if (s == null) { return null; } - final map = (jsonDecode(s as String) as Map).cast(); + final map = jsonDecode(s as String) as Map; return User.fromJson(map); } -String _toString(User u) => u == null ? null : jsonEncode(u); +String? _toString(User? u) => u == null ? null : jsonEncode(u);