Don't invoke onValue for a canceled operation (#177)
Fixes #172
The `isCompleted` boolean does not necessarily indicate that
`valueOrCancellation` will complete with a value. If the completer was
completed with a `Future` and then canceled before that argument
invokes the listener callback, the `valueOrCancellation` future will
complete with `null` but `isCompleted` will be true. We only have a
value to pass to `onValue` when the completer was completed and not
canceled.
Add tests for the behavior of `then` when `cancel` is called after
`completer.complete`, both when it was completed with a Future and a
non-Future value.
Move the initialization for `originalCompleter` to the `setup` for the
tests so that it can be used before calling `then`.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1560001..4680638 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
-## 2.6.2
+## 2.6.2-dev
-* Internal changes only.
+* Fix a bug where `CancelableOperation.then` may invoke the `onValue` callback,
+ even if it had been canceled before `CancelableOperation.value` completes.
## 2.6.1
diff --git a/lib/src/cancelable_operation.dart b/lib/src/cancelable_operation.dart
index c321078..6e6ef17 100644
--- a/lib/src/cancelable_operation.dart
+++ b/lib/src/cancelable_operation.dart
@@ -101,7 +101,7 @@
valueOrCancellation().then((T? result) {
if (!completer.isCanceled) {
- if (isCompleted) {
+ if (isCompleted && !isCanceled) {
assert(result is T);
completer.complete(Future.sync(() => onValue(result as T)));
} else if (onCancel != null) {
diff --git a/test/cancelable_operation_test.dart b/test/cancelable_operation_test.dart
index 0e8d8d4..47a1e91 100644
--- a/test/cancelable_operation_test.dart
+++ b/test/cancelable_operation_test.dart
@@ -267,10 +267,10 @@
onError = expectAsync2((e, s) => 'Fake', count: 0, id: 'onError');
onCancel = expectAsync0(() => 'Fake', count: 0, id: 'onCancel');
propagateCancel = false;
+ originalCompleter = CancelableCompleter();
});
CancelableOperation<String> runThen() {
- originalCompleter = CancelableCompleter();
return originalCompleter.operation.then(onValue!,
onError: onError,
onCancel: onCancel,
@@ -387,6 +387,29 @@
expect(runThen().value, throwsA('error'));
originalCompleter.operation.cancel();
});
+
+ test('after completing with a future does not invoke `onValue`',
+ () async {
+ onValue = expectAsync1((_) => '', count: 0);
+ onCancel = null;
+ var operation = runThen();
+ var workCompleter = Completer<int>();
+ originalCompleter.complete(workCompleter.future);
+ originalCompleter.operation.cancel();
+ workCompleter.complete(0);
+ expect(operation.isCanceled, true);
+ await workCompleter.future;
+ });
+
+ test('after the value is completed invokes `onValue`', () {
+ onValue = expectAsync1((_) => 'foo', count: 1);
+ onCancel = expectAsync1((_) => '', count: 0);
+ originalCompleter.complete(0);
+ originalCompleter.operation.cancel();
+ var operation = runThen();
+ expect(operation.value, completion('foo'));
+ expect(operation.isCanceled, false);
+ });
});
group('returned operation canceled', () {