Iced: Create Subscription from futures::stream::Stream

Created on 9 May 2020  路  10Comments  路  Source: hecrj/iced

I have a situation where I have a futures::stream::Stream which I would like to drive events in iced. I am using iced to receive events from a simulation (which can run on a cluster of computers) and display them.

I am a bit new to async/await, so I don't know the best way to solve this problem, but it seems the only way I can have a streaming async subscription right now is to:

  • On initialize, run a Command::perform that retrieves an item from the stream
  • On each item retrieved from the stream, run another Command::perform that gets the next item

This works, but is really strange. Subscription says that it it for listening to external events, so it makes the most sense to support this in Subscription.

question

All 10 comments

The idea is that you subscribe in your Application to subscriptions you want to listen to which then kick of messages you can deal with in your update function. The download_progress example subscribes to a stream of download events like this:

fn subscription(&self) -> Subscription<Message> {
  match self { 
    Example::Downloading { .. } => {
      download::file("https://speed.hetzner.de/100MB.bin")
               .map(Message::DownloadProgressed)
      }
      _ => Subscription::none(),
  }
}

The magic happens in download.rs where we create a Recipe which creates the stream.

Because you already have Stream just wrapping it into a Recipe and mapping it to an Message should be all you need. Much fun!

Yes, as @Songtronix pointed out you should create the stream in Recipe::stream.

Is it not possible to impl Recipe for S where S is a stream of Message? I checked the examples and it seems a bit excessive just to forward a stream into a message. I just want to do something approximately like:

run().map(Message::FromSim)

Is it possible that this can impl Recipe?

I also have to initiate the connection and store an outgoing sink inside the state of the application as well, and I am not sure how I can get both the tx and rx side to the subscription and new() at the same time.

Check out #336.

You will need to create the stream and the channels in Recipe::stream.

Interesting, so I should send sink over a message back to the main thread. That may work, but it might make the UI elements unresponsive temporarily. Additionally, the transmitter would then need to be added as an Option rather than the real thing. Here is where it is happening:

https://github.com/evomata/evonomics/blob/a57b62baf97c1c8a8fb77977a0db03d47210e380/src/main.rs#L91

As you can see, I use a command that indefinitely recreates itself to simulate the Subscription. This allows me to put the tx and rx side in the new(). It would be nice if there was some way for me to create subscriptions using the Flags, as that would eliminate the issue. Alternatively, if I can make a Command that generates a Subscription, that would also work.

I can probably store the transmitter as an Option, but I would rather the UI be initialized in full before it starts, otherwise I might end up having to use the transmitter through an Option, which will make the code more verbose.

That may work, but it might make the UI elements unresponsive temporarily.

I don't think it should as long as you use async/await with Subscription and Command properly.

Additionally, the transmitter would then need to be added as an Option rather than the real thing.

It's very common in Elm applications to model the state using an enum with Loading and Ready variants.

It would be nice if there was some way for me to create subscriptions using the Flags, as that would eliminate the issue. Alternatively, if I can make a Command that generates a Subscription, that would also work.

Subscriptions themselves are not streams, but just a request to keep a certain stream alive (or create it if it does not exist). This is consistent and plays well with the rest of the immediate architecture. The same way you can stop showing a widget by not returning it in view, dropping a stream is just a matter of not returning its Subscription.

I can probably store the transmitter as an Option, but I would rather the UI be initialized in full before it starts, otherwise I might end up having to use the transmitter through an Option, which will make the code more verbose.

I believe this is the way to go. I'd argue the Option or explicit enum variants are necessary because you are dealing with a concurrent model, not because of the current architecture.

As an alternative, you could choose to store your Sim in your application state and use a Command to compute the next tick just like the game_of_life example. And if you are concerned with cloning the whole world before each tick (I measured this for the game_of_life and it was not even close to being the bottleneck), you could wrap it in an Option and use Option::take.

@hecrj My plan is to run the simulation on multiple machines in the future, so the state will live on other computers. For the time being, I will keep things as they are because it seems to work well and it keeps the concerns in the code roughly where I want them. I do agree that it might be good to have two states for the app (sim running and no sim running), and that the transition between them can just have some lag associated with it or a spinner or something. I will look into spinner widgets or how I can deal with the lag between when the simulation is triggered and the actual UI update.

I will also look into the possibility of creating an API that impls Recipe that sends you back both a stream of messages and the sender over it, as you mentioned. I think that concept is common enough to warrant its own API. If I make that, I will open an issue to see if you would like to include it upstream.

Thanks.

My plan is to run the simulation on multiple machines in the future, so the state will live on other computers.

In this case, you will definitely need Loading and Disconnected variants in your application state.

the transition between them can just have some lag associated with it or a spinner or something

I am not exactly sure what you mean by this, nor how your current approach solves this "lag". There is no need to drop the subscription when the simulation isn't running.

@hecrj Right now the UI is not created until the simulation is ready. However, it likely shouldn't be like that. It should probably start up and there should be a loading indicator visually showing that it is not ready yet. It is also possible that you may want to connect to new simulation sources in the UI later. I will have to investigate ways of making this work with the subscription model. For the time being the simulation is just running locally, so there is no issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jiminycrick picture jiminycrick  路  3Comments

michael-hart picture michael-hart  路  4Comments

aentity picture aentity  路  4Comments

cetra3 picture cetra3  路  3Comments

kszlim picture kszlim  路  4Comments