Skip to content

Instantly share code, notes, and snippets.

@dakom
Last active October 22, 2020 08:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dakom/676b72065fd40975c82e6fd8c7d7dec7 to your computer and use it in GitHub Desktop.
Save dakom/676b72065fd40975c82e6fd8c7d7dec7 to your computer and use it in GitHub Desktop.

Bullet points

  • spawn_local will drop its owned data when the future completes. If the future never completes, it's a memory leak.
  • same with future_to_promise (holding/dropping the promise won't help)
  • ideally a Future should be held so that it can be dropped at any point, thereby avoiding leaks even if the future doesn't complete - e.g. keeping things as proper Rust Futures all the way.

Demos

Cut and paste conversation

PauanToday at 11:45 AM @dakom normally you'd just use spawn_local inside of the Closure if you want it to be cancellable, you'd just use the normal Future cancellation mechanisms like this: https://docs.rs/futures/0.3.4/futures/future/struct.AbortHandle.html futures::future::AbortHandle - Rust API documentation for the Rust AbortHandle struct in crate futures. dakomToday at 11:46 AM seems very similar to the idea of keeping it inside of the Root struct? PauanToday at 11:46 AM no, it's very different you can't even do cancellation with your way since Promises don't support cancellation if you want it to be cancellable, you'd use AbortHandle to wrap your Future, then store the AbortHandle inside the struct and use spawn_local to spawn the Future something like this: struct Foo { handle: AbortHandle, }

impl Foo { fn new() -> Self { let (fut, handle) = abortable(async move { // ... });

    spawn_local(fut);

    Self { handle }
}

} though... I have to say that's a pretty weird way of doing things and isn't what I would do at all what I would do is have a Stream of Click events like an unbounded channel perhaps then when the click happens you'd push the event into the stream and in your main function (or a sub-function of main) you'd use for_each to loop over the Stream dakomToday at 11:51 AM thanks!

this fixes a whole slew of problems: you don't need to spawn_local or deal with abortable, it supports cancellation automatically, you can use throttling or debouncing, it can support backpressure, you can run each event sequentially or in parallel, etc. I've found that Streams are really great, so I try to convert events into Streams ASAP, rather than dealing with events directly and if you're trying to use Futures inside of events, that's all the more reason to convert it into a Stream ASAP since Streams interact really well with Futures, whereas events do not (this is the unbounded channel I was referring to: https://docs.rs/futures/0.3.4/futures/channel/mpsc/fn.unbounded.html ) futures::channel::mpsc::unbounded - Rust API documentation for the Rust unbounded fn in crate futures. here's a great example where I convert an event listener into a Stream: https://github.com/Pauan/tab-organizer/blob/cde6e851b398afd4248e0bf3de3c56baada7fb28/src/lib.rs#L961-L979 GitHub Pauan/tab-organizer Google Chrome Extension that makes it easier to manage many tabs! - Pauan/tab-organizer

I use this technique all the time (this is for a Chrome/Firefox extension, but the same technique works with gloo events, etc.)

PauanToday at 11:59 AM here's an example of actually using the Stream: https://github.com/Pauan/tab-organizer/blob/cde6e851b398afd4248e0bf3de3c56baada7fb28/src/sidebar/src/lib.rs#L408-L446 GitHub Pauan/tab-organizer Google Chrome Extension that makes it easier to manage many tabs! - Pauan/tab-organizer

in this case I'm using try_fold and some other things, but normally you'd just use for_each

note that it's using await since try_fold and for_each return a Future

also note that it's inside of my main function

(which is async, so I don't even need to use spawn_local)

if you need concurrency you can use the usual Future stuff like join

like if you have multiple Streams you can use join to run them all simultaneously

but yeah, Streams are really great, they fix so many issues that events have here's a great example: https://github.com/Pauan/tab-organizer/blob/cde6e851b398afd4248e0bf3de3c56baada7fb28/src/background/src/lib.rs#L1165-L1168 GitHub Pauan/tab-organizer Google Chrome Extension that makes it easier to manage many tabs! - Pauan/tab-organizer

dakomToday at 12:02 PM and for one-offs like "load config.json at page start" - just spawn_local ? PauanToday at 12:02 PM so the way that for_each works, it accepts a closure which returns a Future and it waits for that Future to finish before it goes to the next Stream item

so I'm using that to wait for a particular condition before I continue so that means that when the pending signal is 0, it will just immediately run the code for the Stream elements but if it's not 0, it will then pause the Stream until it is 0 and unlike with events, nothing is lost: all of the events are still there

being able to merge streams together, apply backpressure, delay them, debounce them, throttle them, etc. all of that's easy, whereas doing it with events is a lot harder for one-offs I would just use an async main function https://github.com/Pauan/tab-organizer/blob/cde6e851b398afd4248e0bf3de3c56baada7fb28/src/sidebar/src/lib.rs#L300-L301 GitHub Pauan/tab-organizer Google Chrome Extension that makes it easier to manage many tabs! - Pauan/tab-organizer

(yes it works, I specifically added in support for it in wasm-bindgen) (internally it uses spawn_local, but it's a lot nicer, and it automatically handles errors, unlike spawn_local) dakomToday at 12:07 PM cool... and then inside of that you use streams to trigger state updates? e.g. that's the port.on_message() ? PauanToday at 12:07 PM the port.on_message() is a stream of events from the server

it then processes each event, and updates the app's Signals accordingly (and then when the Signals are updated, dominator automatically updates the DOM) dakomToday at 12:09 PM ok - so basically I had it inverted :slight_smile: (also didn't know about all the other niceties like async main, streams, etc.)

ok - so basically I had it inverted :slight_smile: (also didn't know about all the other niceties like async main, streams, etc.) PauanToday at 12:09 PM how was it inverted? dakomToday at 12:11 PM I had the Mutable container (in this case Root) spawning and containing Futures, whereas it's better to have Streams that contain/affect the Mutable in their callback err not callback, but try_fold, whatever it is that the Streams call on updates :slight_smile: PauanToday at 12:11 PM yeah, that does sound inverted, which's why you were having a tough time

maybe you could make it work, especially if you build up some helper types and infrastructure but I've found that my style works well it just uses normal Rust idioms: structs contained within structs, Mutable for state, Rc to share the top-level struct and then Streams + Futures to handle async stuff maybe it would be good if I created a dominator example which showed off these async techniques dakomToday at 12:13 PM yeah I also painted myself into a corner trying to do things with methods (e.g. render(&self)) and kept getting stuck until I just changed it to render(root: Rc) ... a little more verbose, needing to call Root::render(root) but not so bad PauanToday at 12:13 PM since TodoMVC doesn't really have any of that oooh, yeah, it took me a long time to figure that out too you can actually do fn render(self: Rc) and then it will work as a method call but I don't actually like that because you can't do clone!(self => ...) dakomToday at 12:14 PM

there's a select few types which are allowed as self, including Rc, Arc, and Box it's currently hardcoded, so you can't just do Foo or whatever dakomToday at 12:16 PM TIL 😄 yeah an example would be great. I think something like:

  1. load config at startup
  2. buttons that trigger some async op
  3. cancel button PauanToday at 12:14 PM so I prefer fn render(state: Rc) so that way I can do clone!(state => ...) dakomToday at 12:15 PM didn't realize that other approach was legal, interesting... but yeah, the whole point is needing to clone and the clone! helper is great
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment