-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
It is not easy to chain a CancelableOperation following other work #351
Comments
Ideally, you should never have a I think returning Or maybe it should be: CancelableOperation<R> thenOperation<R>(void Function(T, CancelableCompleter<R>) onValue,
{void Function(Object, StackTrace, CancelableCompleter<R>) onError,
void Function(CancelableCompleter<R>) onCancel}); because having three outcomes means representing them by return or throw is not sufficient. That works for It's never going to be as convenient as |
My CancelableOperation<T> flatten<T>(Future<CancelableOperation<T>> nested) {
CancelableOperation? operation;
var completer = CancelableCompleter<T>(onCancel: () {
operation?.cancel();
});
nested.then<void>((o) {
if (completer.isCanceled) {
o.cancel();
o.value.ignore(); // In case it has already completed with an error.
return;
}
operation = o;
// // if only we could do this, then the `operation` variable wouldn't be needed:
// completer.onCancel = o.cancel;
o.then<void>(completer.complete, onError: completer.completeError, onCancel: completer.operation.cancel);
} , onError: (e, s) {
completer.completeError(e, s);
});
return completer.operation;
} What we need might be a class CancelableCompleter<T> ... {
/// Makes this [CancelableCompleter.operation] complete with the same result as [operation].
///
/// If the operation of this completer is cancelled before [operation] completes,
/// then if [propagateCancel] is set to true, [operation] is also cancelled.
void completeOperation(CancelableOperation operation, {bool propagateCancel = false}) {
if (!_mayComplete) throw StateError("Already completed");
_mayComplete = false;
if (isCanceled) {
if (propagateCancel) operation.cancel();
operation.value.ignore();
return;
}
operation.then<void>((value) {
_inner?.complete(value); // _inner is set to null if this.operation is cancelled.
}, onError: (error, stack) {
_inner?.completeError(error, stack);
}, onCancel: () {
this.operation.cancel();
});
if (propagateCancel) {
_cancelCompleter.future.whenComplete(operation.cancel);
}
} Then flatten would be: CancelableOperation<T> flatten<T>(Future<CancelableOperation<T>> operation) {
var completer = CancelableCompleter<T>();
operation.then(completer.completeOperation);
return completer.operation;
} |
I modeled it as a The real situation was a custom @override
Future<StreamedResponse> send(BaseRequest request) async {
if (!credentials.accessToken.hasExpired) {
return _authClient.send(request);
}
_authClient = await _createAuthorizedClient();
return _authClient.send(request);
} Without await it's still very readable. @override
Future<StreamedResponse> send(BaseRequest request) {
if (!credentials.accessToken.hasExpired) {
return _authClient.send(request);
}
return _createAuthorizedClient().then((client) {
_authClient = client;
return client.send(request);
});
} Once I make it return @override
CancelableOperation<StreamedResponse> send(BaseRequest request) {
if (!credentials.accessToken.hasExpired) {
return _authClient.send(request);
}
CancelableOperation<StreamedResponse>? operation;
final completer = CancelableCompleter<StreamedResponse>(
onCancel: () => operation?.cancel());
createAuthorizedClient().then((client) {
_authClient = client;
operation = client.send(request);
return operation!.valueOrCancellation().then((response) {
if (completer.isCanceled) return;
if (operation!.isCompleted && !operation!.isCanceled) {
completer.complete(response);
} else {
completer.operation.cancel();
}
});
}, onError: (Object error, StackTrace stackTrace) {
if (completer.isCanceled) return;
completer.completeError(error, stackTrace);
});
return completer.operation;
} |
I do like the combination of CancelableOperation<R> thenOperation<R>(
void Function(T, CancelableCompleter<R>) onValue,
{void Function(Object, StackTrace, CancelableCompleter<R>)? onError,
void Function(CancelableCompleter<R>)? onCancel}) { With With these the solution feels pretty similar to the @override
CancelableOperation<StreamedResponse> send(BaseRequest request) {
if (!credentials.accessToken.hasExpired) {
return _authClient.send(request);
}
return CancelableOperation.fromFuture(
_createAuthorizedClient().then((client) => _authClient = client))
.thenOperation((client, completer) {
completer.completeOperation(client.send(request));
});
} |
I was just thinking that it's too bad the more general version can't be named I do think that the |
Towards #210 Once combined with `Completer.completeOperation` this signature adds significant flexibility for chaining Cancelable work following other async work. Move the existing `.then` operation to `.thenOperation` since the latter is more general.
We have a PR in I wondered what it would look like to need to migrate this to a retry around It isn't nice to read, but I don't think it's too far off of what I'd write without @override
CancelableOperation<StreamedResponse> send(BaseRequest request) =>
_send(request, StreamSplitter(request.finalize()), 0);
CancelableOperation<StreamedResponse> _send(
BaseRequest request, StreamSplitter<List<int>> splitter, int attempt) {
void retry(StreamedResponse? response,
CancelableCompleter<StreamedResponse> completer) {
final work = _onRetry?.call(request, response, attempt);
if (work is Future<void>) {
work.then((_) {
completer.completeOperation(_send(request, splitter, ++attempt));
});
} else {
completer.completeOperation(_send(request, splitter, ++attempt));
}
}
return _inner.send(_copyRequest(request, splitter.split())).thenOperation(
(response, completer) {
if (attempt == _retries) {
completer.complete(response);
return;
}
void act(bool when) {
if (when) {
retry(response, completer);
} else {
completer.complete(response);
}
}
final when = _when(response);
if (when is Future<bool>) {
when.then(act);
} else {
act(when);
}
}, onError: (error, stackTrace, completer) {
if (attempt == _retries) {
completer.completeError(error, stackTrace);
return;
}
void act(bool when) {
if (when) {
retry(null, completer);
} else {
completer.completeError(error, stackTrace);
}
}
final when = _whenError(error, stackTrace);
if (when is Future<bool>) {
when.then(act);
} else {
act(when);
}
});
} |
The issue here is that a cancelable operation can complete in three ways, just not two like a An interesting thought is that The cancelable operation is also a delayed sum type, just with three summands (value/error/cancel), and the What if we had a lower-level "wait" operation on a Anyway, for the retry, I'd consider just ignoring the synchronous case and always void retry(StreamedResponse? response,
CancelableCompleter<StreamedResponse> completer) async {
await _onRetry?.call(request, response, attempt);
completer.completeOperation(_send(request, splitter, ++attempt));
}
return _inner.send(_copyRequest(request, splitter.split())).thenOperation(
(response, completer) async {
if (attempt != _retries && await _when(response)) {
await retry(response, completer);
} else {
completer.complete(response);
}
}, onError: (error, stackTrace, completer) async {
if (attempt != _retries && await _whenError(error, stackTrace) {
await retry(null, completer);
} else {
completer.completeError(error, stackTrace);
}
}
}); If something allows returning either a future or a value, I treat it as an asynchronous computation which allows the user to omit wrapping the result in a future. I won't do extra work to ensure that everything runs synchronously if the return value is not a future. I made work.then((_) {
completer.completeOperation(_send(request, splitter, ++attempt)); would be uncaught. If that's deliberate and documented, then it's fine. |
Yeah, we can simplify the implementation if we don't allow cancelling work in between each of the callbacks. I was curious whether there are other building blocks that would make even an extreme case more readable. For now I'll plan on pushing through with the two APIs we've discussed so far. |
Another use case that came up is wanting to add an extra We should consider adding a |
Towards #210 Once combined with `Completer.completeOperation` this signature adds significant flexibility for chaining Cancelable work following other async work. Move the existing `.then` implementation to `.thenOperation` since the latter is more general. Co-authored-by: Lasse R.H. Nielsen <[email protected]>
Towards #210 Combined with `CancelableOperation.thenOperation` this allows chaining cancelable work that can be canceled at multiple points.
Towards #210 Combined with `CancelableOperation.thenOperation` this allows chaining cancelable work that can be canceled at multiple points.
Thanks for the great discussion here. I've been looking into using |
Towards dart-lang/async#210 Once combined with `Completer.completeOperation` this signature adds significant flexibility for chaining Cancelable work following other async work. Move the existing `.then` implementation to `.thenOperation` since the latter is more general. Co-authored-by: Lasse R.H. Nielsen <[email protected]>
Towards dart-lang/async#210 Combined with `CancelableOperation.thenOperation` this allows chaining cancelable work that can be canceled at multiple points.
There is no equivalent of
async
andawait
, and the chaining we have through.then
requiresFutureOr
returns from the callbacks - we can't do any cancelable work inside the callback. These factors mean that some idioms which feel easy withFuture
feel clunky, and require careful use ofCancelableCompleter
.For example, I tried to write a retry when a
CancelableOperation
results in an error. The nicest I could come up with is:In another place I tried to wrap a
CancelableOperation Function()
where it needs some async work before forwarding to a delegate, which is equivalent to flattening aFuture<CancelableOperation>
to aCancelableOperation
.This is coming up because I'm exploring the impact of making
BaseClient.send
returnCancelableOperation
instead ofFuture
for dart-lang/http#424, and many of the clients that wrap a delegate feel more complex after the change.I think the
flatten
utility could be good to add to this package. Theretry
here is simplified and wouldn't work for what we need - I'm not sure it's wise to try to write a general enoughretry
though.cc @lrhn - Can you think of any simpler way to translate these patterns? Can you think of any other building blocks we could add to
CancelableOperation
to make them easier? I wonder if something similar to.then
taking aCancelableOperation<R> Function(T) onValue
instead of aFutureOr<R> Function(T) onValue
would help?The text was updated successfully, but these errors were encountered: