Create and execute state machines and statecharts.
The primary feature of statecharts is that states can be organized in a hierarchy: A statechart is a state machine where each state in the state machine may define its own subordinate state machines, called substates. Those states can again define substates.
https://statecharts.github.io/what-is-a-statechart.html
Statecharts are a formalism for modeling stateful, reactive systems. This is useful for declaratively describing the behavior of your application, from the individual components to the overall application logic.
https://xstate.js.org/docs/
In addition to just using statecharts to model the behaviour in documents separate from the actual running code, it’s possible to use one of various machine formats, both to design the behaviour, and at run-time to actually be the behaviour. The idea is to have a single source of truth that describes the behaviour of a component, and that this single source drives both the actual run-time code, but that it can also be used to generate a precise diagram that visualises the statechart.
Rambling thoughts on React and Finite State Machines
https://www.youtube.com/watch?v=WbhpQXH7XMw
export default Machine(
{
id: "blocktrades",
initial: "Initial",
strict: true,
states: {
Initial: {
on: {
Submit: {
actions: ["createFields", "setStatusToRequesting", "subscribe"],
target: "Submitted"
}
}
},
Submitted: {
invoke: [
{
id: "publisher",
src: publishingMachine,
data: context => context
}
],
on: {
ClientClose: "ClientCloseSent",
SubmitAck: { actions: ["submitAck"], target: "Queued" }
}
},
Queued: {
on: {
ClientClose: "ClientCloseSent",
Expire: "Expired",
PickUp: { actions: ["pickUp"], target: "PickedUp" }
}
},
import tradeModelMachine from "./machines/tradeModelMachine";
import BlockTradeMachineContext from "./machines/BlockTradeMachineContext";
export default function BlockTrade({
...
}) {
const [current, send, service] = useMachine(
tradeModelMachine.withContext({
streamLink: require("service!StreamLink"),
store: require("service!caplinps.redux-store")
})
);
return (
<BlockTradeMachineContext.Provider value={{ current, send, service }}>
<BlockTradeFooterContainer
blockId={blockId}
isBlockValid={isBlockValid}
isQuoting={isQuoting}
isLoading={isLoading}
/>
</BlockTradeMachineContext.Provider>
);
export default function BlockTradeFooter(props) {
const { send } = useContext(BlockTradeMachineContext);
return (
<Button
className="execute-button primary"
onClick={() => {
send("Submit", { blockID: blockId });
}}
disabled={!allowQuoteOrExecution}
>
{executionLabel}
</Button>
)
export default Machine(
{
id: "blocktrades",
initial: "Initial",
strict: true,
states: {
Initial: {
on: {
Submit: {
actions: ["createFields", "setStatusToRequesting", "subscribe"],
target: "Submitted"
}
}
},
{
actions: {
createFields: assign(({ store }, { blockID }) => {
const block = store.getState()["blockTrades"].blocks[blockID];
const isTOBO = hasTOBO(store.getState());
const extraFields = getExtraFields(store, blockID, isTOBO);
const fields = mapBlockToSubmitFields(block, extraFields);
const RequestID = `block-${new Date().getTime()}`;
return { blockID, fields, isTOBO, RequestID };
}),
subscribe: assign({
subscriberActorRef: context => spawn(subscriber(context), "subscriber")
}),
function subscriber({ RequestID, streamLink }) {
return function(callback, receive) {
const subscription = streamLink.subscribe(BLOCK_TRADE_CHANNEL, {
onRecordUpdate(subscription, event) {
const fields = event.getFields();
if (RequestID === fields.RequestID) {
callback({ type: fields.MsgType, fields });
}
},
onSubscriptionStatus() {},
onSubscriptionError() {}
});
receive(event => {
if (event.type === "UNSUBSCRIBE") {
subscription.unsubscribe();
}
});
};
}
Submitted: {
invoke: [
{
id: "publisher",
src: publishingMachine,
data: context => context
}
],
on: {
ClientClose: "ClientCloseSent",
SubmitAck: { actions: ["submitAck"], target: "Queued" }
}
},
export default Machine({
id: "publisher",
initial: "GettingTenorDates",
strict: true,
states: {
GettingTenorDates: {
invoke: { id: "tenordatesgetter", src: getTenorDates },
on: { TenorDatesReceived: "Publishing" }
},
Publishing: {
invoke: { id: "publisher", src: publisher },
on: { Published: "Published" }
},
Published: { type: "final" }
}
});
function getTenorDates({ blockID, store: { dispatch, getState }, streamLink }) {
const block = getState()["blockTrades"].blocks[blockID];
const currencyPair = block.selectedCurrencyPair;
const subject = "/CALENDAR/TENORDATES/" + currencyPair;
return function(callback) {
streamLink.subscribe(subject, {
onSubscriptionStatus() {},
onSubscriptionError(subscription, event) {
dispatch(tenorDatesReceived(blockID, { Tenor: "{}" }));
callback("TenorDatesReceived");
},
onRecordUpdate: function(subscription, event) {
dispatch(tenorDatesReceived(blockID, event.getFields()));
subscription.unsubscribe();
callback("TenorDatesReceived");
}
});
};
}
function publisher({ fields, isTOBO, RequestID, streamLink }) {
return function(callback) {
const fieldsToPublish = {
...fields,
...mandatoryFields,
RequestID,
MsgType: "Submit"
};
if (isTOBO) {
fieldsToPublish.TradingSubProtocol = "SALES_RFS";
}
streamLink.publishToSubject(BLOCK_TRADE_CHANNEL, fieldsToPublish, {
onCommandOk() { callback("Published"); },
onCommandError() { }
});
};
}
state ≈ context
action ≈ event
reducer (+ effects code) ≈ actions