kellpossible / coster

A self-hosted website for sharing costs between people
Other
13 stars 0 forks source link

Asynchronous Actions #14

Open kellpossible opened 4 years ago

kellpossible commented 4 years ago

Database actions might take a long time, same with synchronization actions. Perhaps we need a way to do asynchronous effects.

This article seems to do a fairly comprehensive comparison of the available options related to redux https://sandstorm.de/de/blog/post/async-redux-middleware-comparison.html

redux-loop appears to be the most simple solution, basically, basically just an async call from the reducer which dispatches an action back on the store upon completion.

Originally posted by @kellpossible in https://github.com/kellpossible/coster/issues/12#issuecomment-637979697

kellpossible commented 4 years ago

I'm thinking I'll change the behaviour of the store such that reducer + middleware that produces no events will not notify the listeners. Then I'll introduce perhaps an asynchronous entry point for the store to bypass actions and go straight to notifying? Perhaps a function where you pass in a closure to be executed asynchronously. Alternatively, return a list of async closures to be executed later.

kellpossible commented 4 years ago

Okay, I've decided that perhaps asynchronous effects can be achieved via reducer + events + middleware. The reducer can return a middleware specific event containing a closure/function to execute asynchronously, and the middleware can inject its context into the function when it executes. So, for the database middleware, have a DataFetchEvent(async Fn(store, database)).

A reducer would interact with this by creating a new event:

match action {
    SET_DATA(data) => {
        State {
            ...
            data
        }
    },
    DB_FETCH_DATA => {
        let event = Event::DataFetchEvent(async |store, database| {
            let data = database.get("data").await;
            store.dispatch(SET_DATA(data));
        })
        events.push(event);
        state
    }
}

The middleware can remove the async event so that listeners don't get notified of an event that doesn't change the state.

Alternatively, for this example the store could be enhanced with a new trait?

trait AsyncStore<Action> {
    fn async_dispatch(async Fn(&Store));
}

fn example() {
    store.async_dispatch(async |store| {
        let data = database.get("data").await;
        store.dispatch(SET_DATA(data));
    }
}

Another approach:

enum DatabaseAction {
    SET(key, value),
    GET(key)
}

trait GetDatabaseAction {
    database_action() -> DatabaseAction;
    is_database_action() -> bool;
}

impl GetDatabaseAction for MyAction {
   //  ....
}

In the database middleware keep a reference to the store, and dispatch on the store when the database action has completed.

kellpossible commented 4 years ago

The fluxor project https://github.com/mrpmorris/Fluxor/blob/master/Tutorials/01-BasicConcepts/01B-EffectsTutorial/README.md allows binding async functions/effects to specific actions, which then dispatch back into the store.

kellpossible commented 4 years ago

I like the option of using specific middleware to inject the database into handler functions, otherwise just subscribe to the store events and do things that way, dispatching back into the store.