How to rewrite code with then to await version - dart

I wrote code
pendingFutures.addAll({sqlQuery.hashCode: connection.query(sqlQuery, timeoutInSeconds: 3600)});
var queryResult = await pendingFutures[sqlQuery.hashCode];
var queryObj = RequestCacheStruct(sqlQuery.hashCode, DateTime.now(), queryResult! );
await pendingFutures.remove(sqlQuery.hashCode);
requestsCacheList.add(queryObj);
But it's seems that code above have different behavior than code below:
pendingFutures[sqlQuery.hashCode] =
connection.query(...)
.then((queryResult) {
// TODO: add queryResult into cache.
});
await pendingFutures[sqlQuery.hashCode];
How to fix first code to make it work as second one?
My friend who wrote second code said:
"In the first case, await pendingFutures[...] will return as soon as the request is finished.
In the second - when the request is finished and the record is added to the cache."

I'll try to distill your examples down so that they're proper apples-to-apples comparisons. Given:
theFuture = someAsyncOperation();
var result = await theFuture;
addToCache(result);
additionalWork();
and
theFuture = someAsyncOperation().then((result) => addToCache(result));
await theFuture;
additionalWork();
where theFuture is some non-local variable, then those await theFuture; lines should not result in any material difference: addToCache will be called before additionalWork.
However, a more interesting question (and probably what your friend meant) is that if theFuture is separately awaited:
await theFuture;
yetMoreWork();
will it be guaranteed that yetMoreWork is called after addToCache? In other words: if two .then() callbacks are registered on the same Future, are the callbacks guaranteed to be fired in order of registration? (From what I've observed, it seems that way, but I don't know if it's necessarily true, or even if it is, if it will be guaranteed to be true in the future.)
Back to your original question: how can you rewrite the first form to use only await but still guarantee that operations are serialized in a well-defined manner? You have to create a Future that completes after addToCache is called, and you can do that with a helper function:
Future<void> helperFunction() async {
var result = await someAsyncOperation();
addToCache(result);
}
theFuture = await helperFunction();
additionalWork();

Related

How does Dart treat cascading Futures?

Say I have this class:
void main() async {
final example = ExampleClass();
await example.waitOne();
await example.waitOne();
print('finished');
}
class ExampleClass {
Future<void> waitOne() async {
await Future.delayed(Duration(seconds: 1));
print('1 second');
}
}
This code works exactly as I expect it to. It's output is as follows:
1 second
1 second
finished
Then we have this code:
void main() async {
final example = ExampleClass();
await example
..waitOne()
..waitOne();
print('finished');
}
This code now has cascading operators (..) and the output seems strange:
finished
1 second
1 second
The code skips the two futures and prints "finished" to the console first, then "1 second" gets printed twice at the same time (like Future#wait would do).
Why does Dart act in this way?
In your example with the cascading operator adding await doesn't do anything since the cascade operation doesn't return anything hence there is no future to be awaited and then finished is printed right away
Remember that the result of the cascade operator is the original object that you used it on. That is, for var result = object..x()..y()..z(), result will be assigned the value of object, regardless of what x, y, or z return. The values returned by x(), y(), and z() are ignored. It's the equivalent of:
object.x();
object.y();
object.z();
var result = object;
Your case, which involves Futures, is no different:
final example = ExampleClass();
await example
..waitOne()
..waitOne();
So you're doing the equivalent of:
final example = ExampleClass();
example.waitOne(); // The returned Future is ignored.
example.waitOne(); // The returned Future is ignored.
await example; // Incorrectly using await on a non-Future.
(Note that enabling the unawaited_futures and await_only_futures lints would catch this mistake.)
To properly wait, you can't use the cascade operator and will need to explicitly await the individual operations. Also see the issue Prefix await is cumbersome to work with. which discusses possible changes to the language to support using await with member or cascade operators.

dart - correct coding pattern for subscription when using null saftey?

I've enabled the dart 2.8 experimental null saftey.
I have the following exiting code.
StreamSubscription<String> subscription;
subscription =
response.transform(Utf8Decoder()).transform(LineSplitter()).listen(
(line) async {
result += line;
},
onDone: () async {
unawaited(subscription.cancel());
completer.complete(result);
},
);
With null saftey enabled I get a error in the 'onDone' method where it calls subscription.cancl
"The expression is nullable and must be null-checked before it can be used.
Try checking that the value isn't null before using it.",
I can fix the problem by putting a conditional before the call to cancel, but this seems unnecessary as in reality subscription can never be null.
Is there a coding pattern that allows subscription to be declared as non-null?
The problem here is that the read of subscription happens at a place where it's still potentially unassigned. It isn't, actually, but we only know that because the listen method promises not to call any of the callbacks before returning. The compiler can't see that. So, you need to move the reading to after the assignment.
What I'd do to make this listen call work:
var buffer = StringBuffer(result);
var subscription = response
.transform(Utf8Decoder())
.transform(LineSplitter())
.listen((line) {
buffer.write(line);
});
subscription.onDone(() {
completer.complete(buffer.toString());
});
I removed the async from the callbacks because it is not needed. All it does to make these functions async is to return a future that no-one would ever look at.
In general, the callbacks on Stream and Future should have non-async callbacks.
I also removed the subscription.cancel from the onDone event handler. If you get a "done" event, the subscription is done, there is no need to cancel it.
I also added a string buffer to avoid the quadratic time and space complexity of repeated string concatenation.
Looking at the code, you seem to be concatenating lines right after splitting them, maybe all you need is:
response.transform(Utf8Decoder()).join("").then(completer.complete);
I'll assume for now that the splitting+joining is necessary.
In that case, what I'd actually prefer to do instead is of using listen is:
var buffer = StringBuffer();
response
.transform(Utf8Decoder())
.transform(LineSplitter())
.forEach((line) {
buffer.write(line);
}).then(() {
completer.complete(buffer.toString());
}, onError: (e, s) {
completer.completeError(e, s);
});
or, if in an async function:
try {
var buffer = StringBuffer();
await for (var line in response.transform(Utf8Decoder()).transform(LineSplitter())) {
buffer.write(line);
}
completer.complete(buffer.toString());
} catch(e, s) {
completer.completeError(e, s);
}

Using Futures with Dart

Why is the print result keep returning "asd"?
var jerl = "asd";
HttpRequest.request('Foo',
method: "GET",
requestHeaders: {"Authorization": "Secret"}).then((x) => x.responseText).then((y) => jerl = y);
print(jerl);
Async code is just scheduled for later execution and the sync code continues executing without waiting for the async code. The method you pass to Future.then(...) is executed when the scheduled async code is finished. You find a lot of such questions and examples tagged [:dart-async:] here on StackOverflow.
If you want code to be executed when the async code completed you need to chain it with then like you did with other code
var jerl = "asd";
HttpRequest.request('Foo',
method: "GET",
/* return */ requestHeaders: {"Authorization": "Secret"})
.then((response) {
var jer1 = x.responseText;
print(jerl);
});
You can use the async/await feature to make code look more like sync code
Future someMethod() async { // <== added `Future` and `async`
var jerl = "asd";
var response = await HttpRequest.request('Foo', // <== added `await`
method: "GET",
requestHeaders: {"Authorization": "Secret"}).then((x) =>
print(response.text);
}
async is contagious. There is no way to go back from async execution to sync execution. If your code calls any async method or function and you need code further down to wait for the async result, then you need to chain all following code (also in calling methods) with .then (or use async/await). For this to work you also need to return the future to the caller so he can call .then() on it.
With async/await you don't need to explicitly return the future.
async/await doesn't make async code sync (as mentioned above), it only makes it look more like sync code. The underlying behavior is identical as with then.

Making a Future block until it's done

Is it possible to block a function call that returns a future?
I was under the impression calling .then() does it, but that's not what I'm seeing in my output.
print("1");
HttpRequest.getString(url).then((json) {
print("2");
});
print("3");
What I'm seeing in my output is:
1
3
2
The getString method doesn't have an async that would allow me to await it and then executes asynchronously in any case.
static Future<String> getString(String url,
{bool withCredentials, void onProgress(ProgressEvent e)}) {
return request(url, withCredentials: withCredentials,
onProgress: onProgress).then((HttpRequest xhr) => xhr.responseText);
}
How do I make it blocking without placing an infinite while loop before step 3 waiting for step 2 to be completed (not that it would work anyways due to the single thread nature of Dart)?
The above HttpRequest loads a config.json file that determines how everything works in the app, if the request for a field in the config is done before the config.json file is done loading, it causes errors, so I need to wait until the file is done loading before I allow calling getters on the fields of the class or getters needs to wait for the once-off loading of the config.json file.
Update, this is what I eventually did to make it work after Günter suggested I use a Completer:
#Injectable()
class ConfigService {
Completer _api = new Completer();
Completer _version = new Completer();
ConfigService() {
String jsonURI =
"json/config-" + Uri.base.host.replaceAll("\.", "-") + ".json";
HttpRequest.getString(jsonURI).then((json) {
var config = JSON.decode(json);
this._api.complete(config["api"]);
this._version.complete(config["version"]);
});
}
Future<String> get api {
return this._api.future;
}
Future<String> get version {
return this._version.future;
}
}
And where I use the ConfigService:
#override
ngAfterContentInit() async {
var api = await config.api;
var version = await config.version;
print(api);
print(version);
}
Now I get blocking-like functionality without it actually blocking.
There is no way to block execution until asynchronous code completes. What you can do is to chain successive code so that it is not executed before the async code is completed.
One way to chain is then
print("1");
HttpRequest.getString(url) // async call that returns a `Future`
.then((json) { // uses the `Future` to chain `(json) { print("2"); }`
print("2");
});
print("3"); // not chained and therefore executed before the `Future` of `getString()` completes.
An async call is just scheduling code for later execution. It will be added to the event queue and when the tasks before it are processed it itself will be executed. After an async call is scheduled the sync code `print("3") is continued.
In your case HttpRequest.getString() schedules a call to your server and registers (json) { print("2") as callback to be called when the response from the server arrives. Further execution of the application doesn't stall until the response arrives and there is no way to make that happen. What instead happens is that sync code is continued to be executed (print("3")).
If your currently executed sync code reaches its end, then the next scheduled task is processed the same way.
then() schedules the code (json) { print("2"); } to be executed after getString() completed.
await
async and await just make async code look more like sync code but otherwise it is quite the same and will be translated under the hood to xxx.then((y) { ... }).
I'm not sure I understand what you're trying to achieve, but it looks to me you want to do this:
myFunction() async {
print("1");
final json = await HttpRequest.getString(url);
print("2");
print("3");
}
async statement is only needed in the consumer function. In other words, producer functions doesn't need to have async, they only need to return a Future.
you should be able to do this:
Future consumerFunc() async {
print("1");
var response = await HttpRequest.getString(url);
print("2");
print("3");
}
and it should result:
1
2
3
Note: await replaces then methods

encapsulate repeated send/responses to the same Dart isolate within a single asyncronous function

Is it possible to encapsulate repeated send/responses to the same dart isolate within a single asynchronous function?
Background:
In order to design a convenient API, I would like to have a function asynchronously return the result generated by an isolate, e.g.
var ans = await askIsolate(isolateArgs);
This works fine if I directly use the response generated by a spawnUri call, e.g
Future<String> askIsolate(Map<String,dynamic> isolateArgs) {
ReceivePort response = new ReceivePort();
var uri = Uri.parse(ISOLATE_URI);
Future<Isolate> remote = Isolate.spawnUri(uri, [JSON.encode(isolateArgs)], response.sendPort);
return remote.then((i) => response.first)
.catchError((e) { print("Failed to spawn isolate"); })
.then((msg) => msg.toString());
}
The downside of the above approach, however, is that if I need to repeatedly call askIsolate, the isolate must be spawned each time.
I would instead like to communicate with a running isolate, which is certainly possible by having the isolate return a sendPort to the caller. But I believe since the 2013 Isolate refactoring , this requires the caller to listen to subsequent messages on the receivePort, making encapsulation within a single async function impossible.
Is there some mechanism to accomplish this that I'm missing?
The answer depend on how you intend to use the isolate
Do you intend to keep it running indefinitely, sending it inputs and expecting to receive answers asynchronously?
Do you want to send the isolate many (but finite) inputs at once, expect to receive answers asynchronously, then close the isolate?
I'm guessing the latter, and your askIsolate() function needs to immediately return a Future than completes when it receives all the answers.
The await for loop can be used to listen to a stream and consume events until it closes.
I'm not familiar with isolates, so I hope this is OK, I have not tested it. I've assumed that the isolate terminates and response closes.
String askIsolate(Map<String,dynamic> isolateArgs) async {
ReceivePort response = new ReceivePort();
var uri = Uri.parse(ISOLATE_URI);
Isolate.spawnUri(uri, [JSON.encode(isolateArgs)], response.sendPort)
.catchError((e)) {
throw ...;
});
List<String> answers = new List<String>;
await for(var answer in response) {
out.add(answer.toString());
}
return answers;
}
Note:
response is the stream you are listening to for answers. It's created before spawning the isolate so you don't need to (and probably should not) wait for the isolate future to complete before listening to it.
I made askIsolate() async because that makes it very easy to immediately return a future which completes when the function returns - without all that tedious mucking about with .then(...) chains, which I personally find confusing and hard to read.
BTW, your original then(...).catchError(...) style code would be better written like this:
Isolate.spawnUri(uri, [JSON.encode(isolateArgs)], response.sendPort)
.catchError((e) { ... });
return response.first)
.then((msg) => msg.toString());
I believe that delaying attaching a catchError handler to the line after the isolate's creation might allow the future to complete with an error before the handler is in place.
See: https://www.dartlang.org/articles/futures-and-error-handling/#potential-problem-failing-to-register-error-handlers-early .
I also recommend looking at IsolateRunner in package:isolate, it is intended to solve problems like this - calling a function in the same isolate several times instead of just once when the isolate is created.
If you don't want that, there are other, more primitive, options
Async functions can wait on futures or streams and a ReceivePort is a stream.
For a quick hack, you can probably do something with an await for on the response stream, but it won't be very convenient.
Wrapping the ReceivePort in a StreamQueue from package:async is a better choice. That allows you to convert the individual events into futures. Something like:
myFunc() async {
var responses = new ReceivePort();
var queue = new StreamQueue(responses);
// queryFunction sends its own SendPort on the port you pass to it.
var isolate = await isolate.spawn(queryFunction, [], responses.sendPort);
var queryPort = await queue.next();
for (var something in somethingToDo) {
queryPort.send(something);
var response = await queue.next();
doSomethingWithIt(response);
}
queryPort.send("shutdown command");
// or isolate.kill(), but it's better to shut down cleanly.
responses.close(); // Don't forget to close the receive port.
}
A quick working example based on lrn's comment above follows. The example initializes an isolate via spawnURI, and then communicates with the isolate by passing a new ReceivePort upon which a reply is expected. This allows the askIsolate to directly return a response from a running spawnURI isolate.
Note error handling has been omitted for clarity.
Isolate code:
import 'dart:isolate';
import 'dart:convert' show JSON;
main(List<String> initArgs, SendPort replyTo) async {
ReceivePort receivePort = new ReceivePort();
replyTo.send(receivePort.sendPort);
receivePort.listen((List<dynamic> callArgs) async {
SendPort thisResponsePort = callArgs.removeLast(); //last arg must be the offered sendport
thisResponsePort.send("Map values: " + JSON.decode(callArgs[0]).values.join(","));
});
}
Calling code:
import 'dart:async';
import 'dart:isolate';
import 'dart:convert';
const String ISOLATE_URI = "http://localhost/isolates/test_iso.dart";
SendPort isolateSendPort = null;
Future<SendPort> initIsolate(Uri uri) async {
ReceivePort response = new ReceivePort();
await Isolate.spawnUri(uri, [], response.sendPort, errorsAreFatal: true);
print("Isolate spawned from $ISOLATE_URI");
return await response.first;
}
Future<dynamic> askIsolate(Map<String,String> args) async {
if (isolateSendPort == null) {
print("ERROR: Isolate has not yet been spawned");
isolateSendPort = await initIsolate(Uri.parse(ISOLATE_URI)); //try again
}
//Send args to the isolate, along with a receiveport upon which we listen for first response
ReceivePort response = new ReceivePort();
isolateSendPort.send([JSON.encode(args), response.sendPort]);
return await response.first;
}
main() async {
isolateSendPort = await initIsolate(Uri.parse(ISOLATE_URI));
askIsolate({ 'foo':'bar', 'biz':'baz'}).then(print);
askIsolate({ 'zab':'zib', 'rab':'oof'}).then(print);
askIsolate({ 'One':'Thanks', 'Two':'lrn'}).then(print);
}
Output
Isolate spawned from http://localhost/isolates/test_iso.dart
Map values: bar,baz
Map values: zib,oof
Map values: Thanks,lrn

Resources