Skip to content

Commit

Permalink
Implement CancelableOperation.then (#83)
Browse files Browse the repository at this point in the history
* Implement CancelableOperation.then

* Update version to 2.2.0
  • Loading branch information
TastyPi authored and lrhn committed Apr 5, 2019
1 parent 16c7ecd commit 50a856f
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.2.0

* Add `then` to `CancelableOperation`.

## 2.1.0

* Fix `CancelableOperation.valueOrCancellation`'s type signature
Expand Down
41 changes: 41 additions & 0 deletions lib/src/cancelable_operation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,47 @@ class CancelableOperation<T> {
return completer.future;
}

/// Registers callbacks to be called when this operation completes.
///
/// [onValue] and [onError] behave in the same way as [Future.then].
///
/// If [onCancel] is provided, and this operation is canceled, the [onCancel]
/// callback is called and the returned operation completes with the result.
///
/// If [onCancel] is not given, and this operation is canceled, then the
/// returned operation is canceled.
///
/// If [propagateCancel] is `true` and the returned operation is canceled then
/// this operation is canceled. The default is `false`.
CancelableOperation<R> then<R>(FutureOr<R> Function(T) onValue,
{FutureOr<R> Function(Object, StackTrace) onError,
FutureOr<R> Function() onCancel,
bool propagateCancel = false}) {
final completer =
CancelableCompleter<R>(onCancel: propagateCancel ? cancel : null);

valueOrCancellation().then((T result) {
if (!completer.isCanceled) {
if (isCompleted) {
completer.complete(Future.sync(() => onValue(result)));
} else if (onCancel != null) {
completer.complete(Future.sync(onCancel));
} else {
completer._cancel();
}
}
}, onError: (error, stackTrace) {
if (!completer.isCanceled) {
if (onError != null) {
completer.complete(Future.sync(() => onError(error, stackTrace)));
} else {
completer.completeError(error, stackTrace);
}
}
});
return completer.operation;
}

/// Cancels this operation.
///
/// This returns the [Future] returned by the [CancelableCompleter]'s
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: async
version: 2.1.0
version: 2.2.0

description: Utility functions and classes related to the 'dart:async' library.
author: Dart Team <misc@dartlang.org>
Expand Down
154 changes: 154 additions & 0 deletions test/cancelable_operation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,158 @@ void main() {
expect(completer.isCanceled, isTrue);
});
});

group("then", () {
FutureOr<String> Function(int) onValue;
FutureOr<String> Function(Object, StackTrace) onError;
FutureOr<String> Function() onCancel;
bool propagateCancel;
CancelableCompleter<int> originalCompleter;

setUp(() {
// Initialize all functions to ones that expect to not be called.
onValue = expectAsync1((_) {}, count: 0, id: "onValue");
onError = expectAsync2((e, s) {}, count: 0, id: "onError");
onCancel = expectAsync0(() {}, count: 0, id: "onCancel");
propagateCancel = false;
});

CancelableOperation<String> runThen() {
originalCompleter = CancelableCompleter();
return originalCompleter.operation.then(onValue,
onError: onError,
onCancel: onCancel,
propagateCancel: propagateCancel);
}

group("original operation completes successfully", () {
test("onValue completes successfully", () {
onValue = expectAsync1((v) => v.toString(), count: 1, id: "onValue");

expect(runThen().value, completion("1"));
originalCompleter.complete(1);
});

test("onValue throws error", () {
// expectAsync1 only works with functions that do not throw.
onValue = (_) => throw "error";

expect(runThen().value, throwsA("error"));
originalCompleter.complete(1);
});

test("onValue returns Future that throws error", () {
onValue =
expectAsync1((v) => Future.error("error"), count: 1, id: "onValue");

expect(runThen().value, throwsA("error"));
originalCompleter.complete(1);
});

test("and returned operation is canceled with propagateCancel = false",
() async {
propagateCancel = false;

runThen().cancel();

// onValue should not be called.
originalCompleter.complete(1);
});
});

group("original operation completes with error", () {
test("onError not set", () {
onError = null;

expect(runThen().value, throwsA("error"));
originalCompleter.completeError("error");
});

test("onError completes successfully", () {
onError = expectAsync2((e, s) => "onError caught $e",
count: 1, id: "onError");

expect(runThen().value, completion("onError caught error"));
originalCompleter.completeError("error");
});

test("onError throws", () {
// expectAsync2 does not work with functions that throw.
onError = (e, s) => throw "onError caught $e";

expect(runThen().value, throwsA("onError caught error"));
originalCompleter.completeError("error");
});

test("onError returns Future that throws", () {
onError = expectAsync2((e, s) => Future.error("onError caught $e"),
count: 1, id: "onError");

expect(runThen().value, throwsA("onError caught error"));
originalCompleter.completeError("error");
});

test("and returned operation is canceled with propagateCancel = false",
() async {
propagateCancel = false;

runThen().cancel();

// onError should not be called.
originalCompleter.completeError("error");
});
});

group("original operation canceled", () {
test("onCancel not set", () {
onCancel = null;

final operation = runThen();

expect(originalCompleter.operation.cancel(), completes);
expect(operation.isCanceled, true);
});

test("onCancel completes successfully", () {
onCancel = expectAsync0(() => "canceled", count: 1, id: "onCancel");

expect(runThen().value, completion("canceled"));
originalCompleter.operation.cancel();
});

test("onCancel throws error", () {
// expectAsync0 only works with functions that do not throw.
onCancel = () => throw "error";

expect(runThen().value, throwsA("error"));
originalCompleter.operation.cancel();
});

test("onCancel returns Future that throws error", () {
onCancel =
expectAsync0(() => Future.error("error"), count: 1, id: "onCancel");

expect(runThen().value, throwsA("error"));
originalCompleter.operation.cancel();
});
});

group("returned operation canceled", () {
test("propagateCancel is true", () async {
propagateCancel = true;

await runThen().cancel();

expect(originalCompleter.isCanceled, true);
});

test("propagateCancel is false", () async {
propagateCancel = false;

await runThen().cancel();

expect(originalCompleter.isCanceled, false);
});
});
});
}

0 comments on commit 50a856f

Please sign in to comment.