Error Handling: The locked method will retry indefinitely if retainFutureErrors is set to false and the future fails. A timeout mechanism or a cap on the number of retries would be preferable in this situation because it could result in an infinite loop. Additionally, when the Completer instance is nullified in the catch block, it is not clear why this is done.
Threading: While the code uses a Completer to ensure that only one async function executes at a time, it is not clear how this will work with multiple isolates or threads.
Here are some tests:
import 'dart:async';
/// A lock that ensures that only one async function executes at a time.
class AsyncLock<T> {
/// Creates a new [AsyncLock].
AsyncLock({this.retainFutureErrors = false});
Completer<T>? _completer;
/// Whether to retain errors or allow reentrancy until the Future completes
/// successfully.
final bool retainFutureErrors;
/// Executes the given [function] and returns the value, but ensures that
/// only one async function executes at a time.
Future<T> locked(FutureOr<T> Function() function) async =>
_completer?.future ?? _executeFunction(function);
/// Creates a new [Completer], executes the given [function] and
/// returns the value.
Future<T> _executeFunction(FutureOr<T> Function() function) async {
_completer = Completer<T>();
try {
final result = await function();
_completer!.complete(result);
return result;
// ignore: avoid_catches_without_on_clauses
} catch (error, stackTrace) {
if (retainFutureErrors) {
_completer!.completeError(error, stackTrace);
} else {
_completer = null;
}
rethrow;
}
}
}
class AsyncLockReentryError implements Exception {
@override
String toString() => 'AsyncLockReentryError: Cannot re-enter locked function.';
}
void main() {
group('AsyncLock', () {
test('should execute function when lock is available', () async {
final asyncLock = AsyncLock<int>();
final result = await asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => 1));
expect(result, equals(1));
});
test('should execute multiple functions in sequence', () async {
final asyncLock = AsyncLock<int>();
final firstResult = await asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => 1));
expect(firstResult, equals(1));
final secondResult = await asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => 2));
expect(secondResult, equals(2));
});
test('should execute function after previous lock completes', () async {
final asyncLock = AsyncLock<int>();
final firstResult = await asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => 1));
expect(firstResult, equals(1));
final secondResult = await asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => 2));
expect(secondResult, equals(2));
final thirdResult = await asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => 3));
expect(thirdResult, equals(3));
});
test('should execute function after previous lock completes even with errors', () async {
final asyncLock = AsyncLock<int>(retainFutureErrors: true);
final firstResult = await asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => 1));
expect(firstResult, equals(1));
await expectLater(
asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => throw Exception('error'))),
throwsA(isA<Exception>()),
);
final thirdResult = await asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => 3));
expect(thirdResult, equals(3));
});
test('should throw error when retainFutureErrors is false', () async {
final asyncLock = AsyncLock<int>();
final future = asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => throw Exception('error')));
await expectLater(future, throwsA(isA<Exception>()));
});
test('should return result with error when retainFutureErrors is true', () async {
final asyncLock = AsyncLock<int>(retainFutureErrors: true);
final future = asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => throw Exception('error')));
await expectLater(future, throwsA(isA<Exception>()));
await expectLater(future.catchError((e) => e), isA<Exception>());
});
test('should not allow reentrancy by default', () async {
final asyncLock = AsyncLock<int>();
final future = asyncLock.locked(() => asyncLock.locked(() => Future.value()));
await expectLater(future, throwsA(isA<AsyncLockReentryError>()));
});
test('should allow reentrancy if there is an error', () async {
final asyncLock = AsyncLock<int>(retainFutureErrors: true);
final future = asyncLock.locked(() => asyncLock.locked(() => Future.delayed(const Duration(milliseconds: 500), () => throw Exception('error'))));
});
});
}```