Orchestrating State

Introduction

With ConanJs you should be able to write complex interactions easily. There isn't really a single feature that allows for this, but it really is the combination of many of them.

Some of them we have already seen:

  • Invoking actions directly from the state

  • Chaining actions. All actions in ConanJs return an ASAP, so you can guarantee that you can execute logic immediately after the actions is completed.

  • Composition. Complex use cases many times requires composing actions like, filter, map...

  • Scoping. Being able to isolate state to operate on it will also help with complex logic.

But there is a key feature that we have not explored yet, Reactions.

You can add reactions to your ConanState by calling addDataReaction. The principle of this very simple, you provide with some logic that will be executed every time the state changes.

Complex scenario

Initial atomic state

These example starts with four atomic states.

const stock$ = Conan.light<StockPrice[]>('stock', [{
    id: 'TSLA',
    price: 1000
},{
    id: 'AAPL',
    price: 350
}]);

const stockOrder$ = Conan.light<StockOrder[]>('stockOrders', [{
    stockId: 'AAPL',
    buy: 300,
    sell: 400
},{
    stockId: 'TSLA',
    buy: 900,
    sell: 1200
}]);

const alertsByStock$ = Conan.light<IKeyValuePairs<StockAlert[]>>('alerts', {});

const alertStream$ = alertsByStock$.map<StockAlert[]>(alertsByStock => {
    let newStream: StockAlert[] = [];
    Objects.foreachEntry(alertsByStock, (stockAlerts)=>newStream = [...newStream, ...stockAlerts])
    return newStream.sort((left, right)=>left.timestamp - right.timestamp);
});

stock$ has the code and price for each stock

stockOrder$ has the buy and sell orders that will trigger alarms

alertsByStock$ the list of alerts by stock key based on stock$ and stockOrder$

alertStream$ as we want to show a list of alerts, we use this derived state to build the list of states based on alertsByStock$ (this is explained further below)

Generating the alerts

Alerts are generated based on any change in either the stock$ or the stockOrder$, below you can see the logic for this.

stockOrder$.tuple(stock$).addDataReaction({
    name: `checking alerts`,
    dataConsumer: ([stockOrders, stocks]) => {
        let newAlerts: StockAlert[] = [];
        stockOrders.forEach(stockOrder => {
            const stock = stocks.find(it => it.id === stockOrder.stockId);
            let operation: 'buy' | 'sell' | 'keep';
            if (stock.price >= stockOrder.sell) {
                operation = 'sell';
            } else if (stock.price <= stockOrder.buy) {
                operation = 'buy';
            } else {
                operation = 'keep';
            }
            newAlerts.push({
                operation,
                orderSnapshot: stockOrder,
                stockSnapshot: stock,
                timestamp: Date.now()
            })
        });

        let nextState: IKeyValuePairs<StockAlert[]> = {...alertsByStock$.getData()};
        newAlerts.forEach(newAlert => {
            if (nextState[newAlert.stockSnapshot.id] == null) {
                nextState[newAlert.stockSnapshot.id] = [newAlert];
            } else {
                let alertsForStock: StockAlert[] = nextState[newAlert.stockSnapshot.id];
                let lastAlert = alertsForStock[alertsForStock.length - 1];
                if (
                    !Objects.deepEquals(newAlert.stockSnapshot, lastAlert.stockSnapshot) ||
                    !Objects.deepEquals(newAlert.orderSnapshot, lastAlert.orderSnapshot)
                ) {
                    alertsForStock.push(newAlert);
                }
            }
        })

        alertsByStock$.do.update(nextState);
    }
})

Merging two states with a tuple:

At the top we merge with a tuple the stock orders, and the stock, this generate a new ConanState that will contain an array of two elements, the stock orders and the stock prices, if any of them changes, a new state will be created.

[...] stockOrder$.tuple(stock$) [...]

Adding a reaction:

Immediately after, we add a reaction to the tuple

[...] .addDataReaction({
    name: `checking alerts`,
    dataConsumer: ([stockOrders, stocks])=> {
    [....]
    }
})

Invoking an action:

Note that the bulk of the logic is to decide if based on the new price and order information a new alert to keep / buy or sell should be generated.

The alerts are built on the back of the reaction into:

let nextState: IKeyValuePairs<StockAlert[]>

The alerts are a map of stock code to any the list of alerts they have.

Once this object is built, all we need to do is to update the alerts state.

alertsByStock$.do.update(nextState);

Streaming the alerts

As mentioned at the beginning we ultimately want to see a stream of alerts on the screen.

Note how we easily manage to do this by composing a new state via map from alertsByStock$

const alertStream$ = alertsByStock$.map<StockAlert[]>(alertsByStock => {
    let newStream: StockAlert[] = [];
    Objects.foreachEntry(alertsByStock, (stockAlerts)=>newStream = [...newStream, ...stockAlerts])
    return newStream.sort((left, right)=>left.timestamp - right.timestamp);
});

See the code by yourself

You can see this in action here:

Last updated