Crate pecs

source ·
Expand description

§About

pecs is a plugin for Bevy that allows you to execute code asynchronously by chaining multiple promises as part of Bevy’s ecs environment.

pecs stands for Promise Entity Component System.

§Features

  • Promise chaining with then()/ then_repeat()
  • State passing (state for promises is like self for items).
  • Complete type inference (the next promise knows the type of the previous result).
  • Out-of-the-box timer, UI and HTTP promises via stateless asyn mod and stateful state.asyn() method.
  • Custom promise registration (add any asynchronous function you want!).
  • System parameters fetching (promise asyn! functions accept the same parameters as Bevy systems do).
  • Nested promises (with chaining, obviously).
  • Combining promises with any/all for tuple/vec of promises via stateless any() /all() methods or stateful state.any()/state.all() methods.
  • State mapping via with(value)/ map(func) (changes state type over chain calls).
  • Result mapping via with_result(value)/ map_result(func) (changes result type over chain calls).

§Example

use bevy::prelude::*;
use pecs::prelude::*;
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(PecsPlugin)
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands, time: Res<Time>) {
    let start = time.elapsed_seconds();
    commands
        // create PromiseLike chainable commands
        // with the current time as state
        .promise(|| start)

        // will be executed right after current stage
        .then(asyn!(state => {
            info!("Wait a second..");
            state.asyn().timeout(1.0)
        }))

        // will be executed after in a second after previous call
        .then(asyn!(state => {
            info!("How large is is the Bevy main web page?");
            state.asyn().http().get("https://bevyengine.org")
        }))

        // will be executed after request completes
        .then(asyn!(state, result => {
            match result {
                Ok(response) => info!("It is {} bytes!", response.bytes.len()),
                Err(err) => info!("Ahhh... something goes wrong: {err}")
            }
            state.pass()
        }))

        // will be executed right after the previous one
        .then(asyn!(state, time: Res<Time> => {
            let duration = time.elapsed_seconds() - state.value;
            info!("It tooks {duration:0.2}s to do this job.");
            info!("Exiting now");
            asyn::app::exit()
        }));
}

There is otput of the above example, pay some attention to time stamps:

18.667 INFO bevy_render::renderer: AdapterInfo { ... }
18.835 INFO simple: Wait a second..
19.842 INFO simple: How large is is the Bevy main web page?
19.924 INFO simple: It is 17759 bytes!
19.924 INFO simple: It tooks 1.09s to do this job.
19.924 INFO simple: Exiting now

§Basics

To create a new promise, you can use one of the following methods:

It is also possible to create PromiseLike promise containers that act just like promises with:

The resulting promise has the signature Promise<S, R>, where R is the type of the result and S is the type of the promise state. Note that R and S must be 'static types, so references or lifetime types are not allowed.

Promises can be chained together using the then() method, which takes an Asyn function created with the asyn! macro. The Asyn function takes the promise state as its first argument, and the promise result as its second argument. Any additional arguments are optional and correspond to the system parameters used in Bevy’s ECS. This allows you to do inside an Asyn function everything you can do inside a regular system, while still keeping track of system parameter states.

If the result of the Asyn function is Resolve, the result is passed immediately to the next promise in the chain. If the result is Await, the next promise in the chain is resolved when the nested promise is resolved. The type of the next promise’s state and result arguments are inferred from the result of the previous promise:

use bevy::prelude::*;
use pecs::prelude::*;
fn inference(mut commands: Commands) {
    commands.add(
        Promise::start(asyn!(_ => {
            Promise::resolve("Hello!")
        }))
         
        // _: PromiseState<()>, result: &str
        .then(asyn!(_, result => {
            info!("#1 resolved with {}", result);
            Promise::resolve("Hello?")
        }))
        // _: PromiseState<()>, result: &str
        .then(asyn!(_, result => {
            info!("#2 resolved with {result}");
            Promise::resolve(result.to_string())
        }))
        // ok_then used to take successfull results
        // _: PromiseState<()>, result: String
        .then(asyn!(_, result => {
            info!("#3 resolved with {result}");
            Promise::resolve(result)
        }))
        // asyn::timeout(d) returns Promise<(), (), ()>
        // that resolves after after `d` seconds
        .then(asyn!(_, result => {
            info!("#4 resolved with {result}");
            asyn::timeout(1.)
        }))
        // _: PromiseState<()>, result: ()
        .then(asyn!(_, result => {
            info!("#5 resolved with {result:?}");
            Promise::resolve(())
        }))
    );
}

§State

When working with asynchronous operations, it is often useful to carry a state along with the promises in a chain. The pecs provides a convenient way to do this using the PromiseState<S> type.

PromiseState is a wrapper around a 'static S value. This value can be accessed and modified using state.value. PromiseState also implements Deref, so in most you cases you can omit .value.

To use PromiseState, you don’t create it directly. Instead, it is automatically passed as the first argument to the Asyn function.

For example, suppose you have a stateful promise that increments a counter, waits for some time, and then logs the counter value. Here’s how you could implement it:

use bevy::prelude::*;
use pecs::prelude::*;
 
fn setup(mut commands: Commands) {
    commands
        // create a promise with int state
        .promise(|| 0)
        .then(asyn!(state => {
            state.value += 1;
            state.asyn().timeout(1.0)
        }))
        .then(asyn!(state => {
            info!("Counter value: {}", state.value);
        }));
}

In this example, we start with an initial state value of 0 and increment it by 1 in the first promise. We then use state.asyn().timeout() to wait for one second before logging the final state value. The asyn method returns an AsynOps<S> value, which can be used to create new promises that are associated with the current state.

PromiseState can be used with other pecs constructs like then(), repeat() or all() to create complex promise chains that carry stateful values. Here’s an example that uses any method to create a promise that resolves when any of provided promises have resolved with current state itself:

use bevy::prelude::*;
use pecs::prelude::*;
 
fn setup(mut commands: Commands, time: Res<Time>) {
    let start = time.elapsed_seconds();
    commands
        // use `start: f32` as a state
        .promise(|| start)
        // state is f32 here
        .then(asyn!(state => {
            state.any((
                asyn::timeout(0.4),
                asyn::http::get("https://bevyengine.org").send()
            ))
        }))
        // state is f32 as well
        .then(asyn!(state, (timeout, response) => {
            if timeout.is_some() {
                info!("Bevy site is not fast enoutgh");
            } else {
                let status = if let Ok(response) = response.unwrap() {
                    response.status.to_string()
                } else {
                    format!("Error")
                };
                info!("Bevy respond pretty fast with {status}");
            }
            // pass the state to the next call
            state.pass()
        }))
        // it is still f32
        .then(asyn!(state, time: Res<Time> {
            let time_to_process = time.elapsed_seconds() - state.value;
            info!("Done in {time_to_process:0.2}s");
        }));
}

See combine_vecs and confirmation examples to better understand the state behaviour.

§Work in Progress

This crate is pretty young. API could and will change. App may crash. Some promises could silently drop. Documentation is incomplete.

But. But. Examples works like a charm. And this fact gives us a lot of hope.

There are a lot docs planned to put here, but I believe it is better to release something then perfect.

Modules§

  • Core Promise functionality.
  • Make http requests asyncroniusly via ehttp
  • All you need is use pecs::prelude::*
  • Defers promise resolving for a fixed amount of time