Refactoring: extract throttling logic

This commit is contained in:
Ming Ming 2021-08-01 01:28:08 +08:00
parent 8cb56e095c
commit 36e5a1c17b
2 changed files with 110 additions and 25 deletions

View file

@ -9,6 +9,7 @@ import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/throttler.dart';
import 'package:nc_photos/use_case/scan_dir.dart'; import 'package:nc_photos/use_case/scan_dir.dart';
abstract class ScanDirBlocEvent { abstract class ScanDirBlocEvent {
@ -119,6 +120,13 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent); AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdatedEvent);
_fileRemovedEventListener.begin(); _fileRemovedEventListener.begin();
_filePropertyUpdatedEventListener.begin(); _filePropertyUpdatedEventListener.begin();
_refreshThrottler = Throttler(
onTriggered: (_) {
add(_ScanDirBlocExternalEvent());
},
logTag: "ScanDirBloc.refresh",
);
} }
static ScanDirBloc of(Account account) { static ScanDirBloc of(Account account) {
@ -163,7 +171,7 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
close() { close() {
_fileRemovedEventListener.end(); _fileRemovedEventListener.end();
_filePropertyUpdatedEventListener.end(); _filePropertyUpdatedEventListener.end();
_propertyUpdatedSubscription?.cancel(); _refreshThrottler.clear();
return super.close(); return super.close();
} }
@ -209,7 +217,10 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
// no data in this bloc, ignore // no data in this bloc, ignore
return; return;
} }
add(_ScanDirBlocExternalEvent()); _refreshThrottler.trigger(
maxResponceTime: const Duration(seconds: 3),
maxPendingCount: 10,
);
} }
void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) { void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) {
@ -226,28 +237,19 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
return; return;
} }
_successivePropertyUpdatedCount += 1; if (ev.hasAnyProperties([
_propertyUpdatedSubscription?.cancel(); FilePropertyUpdatedEvent.propIsArchived,
// only trigger the event on the 10th update or 10s after the last update FilePropertyUpdatedEvent.propOverrideDateTime,
if (_successivePropertyUpdatedCount % 10 == 0) { ])) {
add(_ScanDirBlocExternalEvent()); _refreshThrottler.trigger(
maxResponceTime: const Duration(seconds: 3),
maxPendingCount: 10,
);
} else { } else {
if (ev.hasAnyProperties([ _refreshThrottler.trigger(
FilePropertyUpdatedEvent.propIsArchived, maxResponceTime: const Duration(seconds: 10),
FilePropertyUpdatedEvent.propOverrideDateTime, maxPendingCount: 10,
])) { );
_propertyUpdatedSubscription =
Future.delayed(const Duration(seconds: 2)).asStream().listen((_) {
add(_ScanDirBlocExternalEvent());
_successivePropertyUpdatedCount = 0;
});
} else {
_propertyUpdatedSubscription =
Future.delayed(const Duration(seconds: 10)).asStream().listen((_) {
add(_ScanDirBlocExternalEvent());
_successivePropertyUpdatedCount = 0;
});
}
} }
} }
@ -286,8 +288,7 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
late AppEventListener<FilePropertyUpdatedEvent> late AppEventListener<FilePropertyUpdatedEvent>
_filePropertyUpdatedEventListener; _filePropertyUpdatedEventListener;
int _successivePropertyUpdatedCount = 0; late Throttler _refreshThrottler;
StreamSubscription<void>? _propertyUpdatedSubscription;
bool _shouldCheckCache = true; bool _shouldCheckCache = true;

84
lib/throttler.dart Normal file
View file

@ -0,0 +1,84 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/int_util.dart';
/// Throttle how many times an event could be triggered
///
/// Events can be filtered by 2 ways:
/// 1. Time passed after the last event
/// 2. Number of events
class Throttler<T> {
Throttler({
required this.onTriggered,
this.logTag,
});
/// Post an event
///
/// [data] can be used to provide optional data. When the stream of events
/// eventually trigger, a list of all data received will be passed to the
/// callback. Nulls are ignored
void trigger({
Duration maxResponceTime = const Duration(seconds: 1),
int? maxPendingCount,
T? data,
}) {
_count += 1;
if (data != null) {
_data.add(data);
}
_subscription?.cancel();
_subscription = null;
if (maxPendingCount != null) {
_maxCount = math.min(maxPendingCount, _maxCount ?? int32Max);
}
if (_maxCount != null && _count >= _maxCount!) {
_log.info("[trigger]$_logTag Triggered after $_count events");
_doTrigger();
} else {
final responseTime = _minDuration(
maxResponceTime, _currentResponseTime ?? Duration(days: 1));
_subscription = Future.delayed(responseTime).asStream().listen((event) {
_log.info("[trigger]$_logTag Triggered after $responseTime");
_doTrigger();
});
_currentResponseTime = responseTime;
}
}
/// Drop all pending triggers, this may be useful in places like [dispose]
void clear() {
_subscription?.cancel();
_subscription = null;
_currentResponseTime = null;
_count = 0;
_maxCount = null;
_data = <T>[];
}
void _doTrigger() {
onTriggered?.call(_data);
clear();
}
String get _logTag => logTag == null ? "" : "[$logTag]";
final ValueChanged<List<T>>? onTriggered;
/// Extra tag printed with logs from this class
final String? logTag;
StreamSubscription<void>? _subscription;
Duration? _currentResponseTime;
int _count = 0;
int? _maxCount;
var _data = <T>[];
static final _log = Logger("throttler.Throttler");
}
Duration _minDuration(Duration a, Duration b) {
return a.compareTo(b) < 0 ? a : b;
}