ReScope Stores

September 16, 2019 ยท View on GitHub

_ this is a draft wip doc _

Stores Workflow

  • A Store have it's state updated ( action has pushed state mutation or a source store had its data updated )
  • If this state have the required & followed value
  • The apply function push new data in an async or sync way
  • The store is stabilized and (if there is new data) propagated
  • listening stores have theirs state updated and we go to step 1 until the whole scope is stable

Stability (the Store:apply function)

When state updates occurs, a store state stop being coherent with its results data.
In ReScope stores, the role of the "apply" function is to make the store data predictable basing on its state.

This can be done in 2 way :

  • In a synchronized way, by returning a new data hashmap from the "apply" fn

/*...*/
    apply(data, new_state, changes_in_state){
        let newData = {...data, ...state};
        /* do synchrone interpolation & remaps in newData here */
        return newData;
    }
/*...*/

  • In an async way :
    • using this.push(newData), modifying this.data or by returning a data referenced hashmap
    • and mostly by using wait() & release() paired functions, which will keep the store unstable and lock its propagation until its locks reach 0 (all wait() calls becomes released).

/*...*/
    apply(data={}, new_state, changes_in_state){
        let {async1={}, async2={}, async3={}} = data;
        /* do async fill / update the data */
        this.wait('anAsyncRabbit')
        doSomeAsync(
            (err,res)=>{
                async1.datum=err||res;
                this.release('anAsyncRabbit')
            }
        )

        return {async1, async2, async3};
    }
/*...*/
    apply(data={}, new_state, changes_in_state){
        this.wait('anAsyncRabbit')
        doSomeAsync(
            (err,res)=>{
                this.push({async1:res, status:err||'ok'};// will replace data
                this.release('anAsyncRabbit')
            }
        )

        return data;
    }
/*...*/

About state uptates & theirs propagation

The more a store have sources, the more it state update will be applied first
This is required to :

  • Keep leafs stores sync & coherent with their sources
  • Apply merged state updates to root stores
  • Keep the global app state coherent

This mean whatever the number of stores & the complexity of the deps,
updating a store state will update its synchrone listening stores immediately.

Stores initial state / data

A store initialized with data will be stable synchronously when instantiated.
If it only have a state but no data, the apply function will be called by the constructor synchronously.

Stores initial state & data

Serialization & restoration are managed by the Scopes objects.
Stores only have to maintain the state-data coherence, but can have initial state and data from different sources :

  • Using initial state & data :
class MyStore extends Store {
        static state = {};// initial state (soft cloned)
        static data = {};// initial data (soft cloned)

        state = {
        // instance initial state (merged over cfg & static definitions)
        }
        data = {}// precedence over cfg.data
};

let MyStoreInstance = new MyStore(
        BaseScope,
        {
            state : {}, // merged over the static state
            data  : {}// precedence over static data
        }
)

Actions & mutations

The app state could be mutated using different methods depending the needs.

_ * As the store stay independents, they deal with theirs own perimeters; calling an action will trigger all the active stores actions in the current & parent Scopes that match. _

Using actions

Actions could be dispatched from scopes or directly on the stores.

* They are only called if theirs stores are active.

class AppState extend Store{
        static use     = ["!AppConfig"];// require AppConfig to be applied & propagated
        static actions = {
            activateSalt(arg){// binded on the store instance

                // this.nextState contain the current incoming state
                // this.state contain the last stabilized state

                // return some state updates
                return {some:'mutations'};
                // or
                return; // to not change the state
                // wait, release, setState & push remain callable
                // this.nextState contain the incoming state
            }
        }
    }

Using setState

All stores inherit the setState method.
Once a store state is updated, the resulting data changes are automatically propagated to the followers.

Using stores functions

The stores could be enhanced with functions & setters, that will ultimately update theirs state-data pairing.

  • Usefull to use stores as controllers
class AppState extend Store{
        static use     = ["!AppConfig"];// require AppConfig to be applied & propagated
        static data    = {};

        switchTodoList(todoUrl){
             this.setState({todoUrl})
             // or
             this.wait();
             doSomeAsync(()=>{
                this.data.stateChange = "stand"
                this.release()
             })
        }

    }

push

Using push will update & propag the data of a store.

  • This should be used with cautious as it could break the state-data coherence. (that said not all the stores needs to be predictable)

How to add dependencies in a store

export default class currentUser extends Store {
        static use = ["appState", "session"];// here the source store that should be in the store scope

        apply( data, { appState, session }, changes ) {
            /*...*/
            return data;
        }
};

How to "remap" dependencies value & sub-values in the state/data

export default class myInterpolatedDataStore extends Store {
        static use = {
                "someSource.someValue"          : "mySwitchValue",
                "someSource2.someStuff.value"   : "mySwitchValue2",
                "someSource3.someValue"         : "mySwitchValue3",
                "someSource4.someValue"         : "mySwitchValue4"
        };

        apply( data, { mySwitchValue, mySwitchValue2, mySwitchValue3, mySwitchValue4 }, changes ) {
            /*...*/
            return data;
        }
};

How to keep a store unstable until some stores / value is initialized

export default class myInterpolatedDataStore extends Store {

        static use = {
                "!someSource.someValue"  : "mySwitchValue",// require someSource.someValue != false
                "!someRequieredSource"   : true,
                "someSource2"            : "someSource2"
        };
        // or
        // static use = ["!some.stuff.withPath"]; // bind "withPath" value from the some or stuff store

        apply( data, { mySwitchValue, mySwitchValue2, mySwitchValue3, mySwitchValue4 }, changes ) {
            /*...*/
            return data;
        }
};

How to only call apply & update the store if specific changes occurs in the sources store

export default class myInterpolatedDataStore extends Store {

        static use = {
                "!someSource.someValue"  : "mySwitchValue",// require someSource.someValue != false
                "!someRequieredSource"   : true,
                "someSource2"            : "someSource2"
        };

        static follow = {// only call "apply" if one of these state keys has change
            "someSource2":(newData)=>returnTrueIfApplicable(newdata),
            "mySwitchValue":true, // just change

        }

        apply( data, { mySwitchValue, someSource2 }, changes ) {
            /*...*/
            return data;
        }
};

How to choose if data changes should be applied

export default class myInterpolatedDataStore extends Store {

        static use = {
                "!someSource.someValue"  : "mySwitchValue",// require someSource.someValue != false
                "!someRequieredSource"   : true,
                "someSource2"            : "someSource2"
        };

        hasDataChange( newDatas ) {
            return super.hasDataChange(state);// default : compare old and new data & data.*
        }

        apply( data, { mySwitchValue, someSource2 }, changes ) {
            /*...*/
            return data;
        }
};

How to choose if data changes should be propagated

export default class myInterpolatedDataStore extends Store {

        static use = {
                "!someSource.someValue"  : "mySwitchValue",// require someSource.someValue != false
                "!someRequieredSource"   : true,
                "someSource2"            : "someSource2"
        };

        shouldPropag(data){
            return true;
        }

        apply( data, { mySwitchValue, someSource2 }, changes ) {
            /*...*/
            return data;
        }
};

How to catch synchrone errors in the apply fn

( global error catch based, eg. not using try catch )

export default class testErrorCatch extends Rescope.Store {
   static state = { ok: true };

   static actions = {
       makeError: v => ({ failNow: true })
   };

   apply( data, state ) {
       if ( state.failNow )
           throw new Error("oups")
       return state;
   }

   handleError(error) {
       this.push({ failNow: false, catched: true })
   }
}

How to catch hot reload/switch of stores

  • called after switching the store prototype
export default class testErrorCatch extends Rescope.Store {

   __onHotReloaded(newStoreClass) {
       this.push({ failNow: false, catched: true })
   }
}