diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ddb14..aef42c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.6 + +- Added `UniqueCaller` and `parseIntList`. +- Null safety migration adjustments. + ## 3.0.5 - Null safety migration adjustments. diff --git a/lib/src/events.dart b/lib/src/events.dart index 554a95b..1d5330f 100644 --- a/lib/src/events.dart +++ b/lib/src/events.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:swiss_knife/src/collections.dart'; +import 'math.dart'; + class _ListenSignature { final Object _identifier; @@ -322,7 +324,7 @@ class EventStream implements Stream { ListenerWrapper? listenOneShot(void Function(T event) onData, {Function? onError, void Function()? onDone, - required bool cancelOnError, + bool cancelOnError = false, Object? singletonIdentifier, bool? singletonIdentifyByInstance = true}) { var listenerWrapper = ListenerWrapper(this, onData, @@ -421,20 +423,21 @@ class EventStream implements Stream { class EventStreamDelegator implements EventStream { EventStream? _eventStream; - final EventStream Function()? _eventStreamProvider; + final EventStream? Function()? _eventStreamProvider; EventStreamDelegator(EventStream eventStream) : _eventStream = eventStream, _eventStreamProvider = null; - EventStreamDelegator.provider(EventStream Function() eventStreamProvider) + EventStreamDelegator.provider(EventStream? Function() eventStreamProvider) : _eventStream = null, _eventStreamProvider = eventStreamProvider; /// Returns the main [EventStream]. EventStream? get eventStream { if (_eventStream == null) { - _eventStream = _eventStreamProvider!(); + _eventStream = + _eventStreamProvider != null ? _eventStreamProvider!() : null; if (_eventStream != null) { flush(); } @@ -1274,3 +1277,139 @@ class ListenerWrapper { _subscription = null; } } + +/// Ensures that a call is executed only 1 per time. +class UniqueCaller { + final FutureOr Function() function; + + final void Function(UniqueCaller caller)? onDuplicatedCall; + + final _IdentifierWrapper _identifier; + + static String stackTraceIdentifier([int stackOffset = 0]) { + var stackTracer = StackTrace.current; + var stackTraceStr = stackTracer.toString(); + + var lines = stackTraceStr.split(RegExp(r'[\r\n]+', multiLine: false)); + + var start = Math.min(1 + stackOffset, lines.length - 1); + var end = Math.min(3, lines.length); + + lines = lines.sublist(start, Math.max(start, end)); + + var s = lines.join('\n'); + return s; + } + + UniqueCaller(this.function, + {Object? identifier, + StackTrace? stackTraceIdentifier, + this.onDuplicatedCall}) + : _identifier = _IdentifierWrapper(identifier ?? + stackTraceIdentifier?.toString() ?? + UniqueCaller.stackTraceIdentifier()); + + _IdentifierWrapper get identifier => _identifier; + + static final Set<_IdentifierWrapper> _calling = {}; + + static final Map<_IdentifierWrapper, Future> _callsFuture = {}; + + static Future getCallFuture(Object identifier) { + var identifierWrapper = _IdentifierWrapper(identifier); + return _callsFuture[identifierWrapper] as Future; + } + + static List get calling { + return _callsFuture.values.toList(); + } + + Future callAsync() async { + if (_calling.contains(_identifier)) { + if (onDuplicatedCall != null) onDuplicatedCall!(this); + return Future.value(null); + } + + _calling.add(_identifier); + try { + var ret = function(); + if (ret is Future) { + _callsFuture[_identifier] = ret as Future; + return await ret; + } else { + return ret; + } + } finally { + _finalizeCall(); + } + } + + R? call() { + if (_calling.contains(_identifier)) { + if (onDuplicatedCall != null) onDuplicatedCall!(this); + return null; + } + + _calling.add(_identifier); + try { + var ret = function(); + if (ret is Future) { + var future = ret as Future; + _callsFuture[_identifier] = future; + future.then((value) { + _finalizeCall(); + return value; + }); + return null; + } else { + _finalizeCall(); + return ret; + } + } catch (e) { + _finalizeCall(); + rethrow; + } + } + + void _finalizeCall() { + // ignore: unawaited_futures + _callsFuture.remove(_identifier); + _calling.remove(_identifier); + } +} + +class _IdentifierWrapper { + final Object identifier; + + _IdentifierWrapper(this.identifier); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _IdentifierWrapper && + runtimeType == other.runtimeType && + _identical(identifier, other.identifier); + + bool _identical(Object? o1, Object? o2) { + if (o1 == null && o2 == null) return true; + if (identical(o1, o2)) return true; + if (o1 != null && o2 == null) return false; + if (o1 == null && o2 != null) return false; + + if (o1.runtimeType != o2.runtimeType) return false; + + if (o1 is Future || o1 is Function) { + return false; + } else { + return o1 == o2; + } + } + + @override + int get hashCode => identifier.hashCode; + + @override + String toString() { + return '_IdentifierWrapper{hashCode: $hashCode ; identifier: $identifier}'; + } +} diff --git a/lib/src/math.dart b/lib/src/math.dart index 0789ec5..20cad72 100644 --- a/lib/src/math.dart +++ b/lib/src/math.dart @@ -314,6 +314,23 @@ int? parseInt(Object? v, [int? def]) { return n as int? ?? def; } +/// Parses [l] as [List]. +/// +/// [def] The default value if [l] is invalid. +List? parseIntList(Object? l, [List? def]) { + if (l == null) return def; + + if (l is List) { + var l2 = l.map((e) => parseInt(e)).whereType().toList(); + return l2.isNotEmpty ? l2 : (def ?? l2); + } else if (l is String) { + var l2 = parseIntsFromInlineList(l, _REGEXP_SPLIT_COMMA); + return l2 != null && l2.isNotEmpty ? l2 : (def ?? l2); + } else { + return def; + } +} + /// Parses [v] to [double]. /// /// [def] The default value if [v] is invalid. diff --git a/lib/src/resource.dart b/lib/src/resource.dart index d714197..b8fb0ff 100644 --- a/lib/src/resource.dart +++ b/lib/src/resource.dart @@ -329,7 +329,7 @@ class ContextualResource> context = _resolveContext(resource, context); static List> toList>( - Iterable resources, C Function(T resource) context) => + Iterable resources, C? Function(T resource) context) => resources.map((r) => ContextualResource(r, context)).toList(); @override @@ -482,14 +482,14 @@ class ContextualResourceResolver> { var low = 0; var high = options.length - 1; - var comparator = contextComparator as int Function(C?, C)?; + var comparator = contextComparator; while (low <= high) { var mid = (low + high) ~/ 2; var midVal = options[mid]; var cmp = comparator != null - ? comparator(midVal.context, context) + ? comparator(midVal.context!, context) : midVal.compareContext(context); if (cmp < 0) { diff --git a/pubspec.yaml b/pubspec.yaml index f512fe7..aef6b00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: swiss_knife description: Dart Useful Tools - collections, math, date, uri, json, events, resources, regexp, etc... -version: 3.0.5 +version: 3.0.6 homepage: https://github.com/gmpassos/swiss_knife environment: diff --git a/test/swiss_knife_events_test.dart b/test/swiss_knife_events_test.dart index 2f70ef8..bfc2fbe 100644 --- a/test/swiss_knife_events_test.dart +++ b/test/swiss_knife_events_test.dart @@ -6,6 +6,18 @@ Future _sleep(int delayMs) async { await Future.delayed(Duration(milliseconds: delayMs), () {}); } +void _asyncCall(List calls) async { + var callID = DateTime.now().microsecondsSinceEpoch; + calls.add('$callID> a'); + await Future.delayed(Duration(seconds: 2)); + calls.add('$callID> b'); +} + +void _doUniqueCall(List calls) async { + var uniqueCaller = UniqueCaller(() => _asyncCall(calls)); + uniqueCaller.call(); +} + void main() { group('Events', () { setUp(() {}); @@ -68,5 +80,39 @@ void main() { expect(interactionCompleter.isTriggerScheduled, isFalse); expect(counter.value, equals(1)); }); + + test('NON UniqueCaller', () async { + var calls = []; + + expect(UniqueCaller.calling, isEmpty); + + _asyncCall(calls); + _asyncCall(calls); + + expect(UniqueCaller.calling, isEmpty); + + print('NON UniqueCaller> $calls'); + + expect(calls.where((e) => e.contains(' a')).length, equals(2)); + }); + + test('UniqueCaller', () async { + var calls = []; + + expect(UniqueCaller.calling, isEmpty); + + _doUniqueCall(calls); + _doUniqueCall(calls); + + expect(UniqueCaller.calling.length, equals(1)); + + await Future.wait(UniqueCaller.calling); + + expect(UniqueCaller.calling.length, equals(0)); + + print('UniqueCaller> $calls'); + + expect(calls.where((e) => e.contains(' a')).length, equals(1)); + }); }); }