Skip to content

Instantly share code, notes, and snippets.

@petebarnett
Created September 6, 2021 16:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save petebarnett/d7125b0951a1205e92738f1489bab749 to your computer and use it in GitHub Desktop.
Save petebarnett/d7125b0951a1205e92738f1489bab749 to your computer and use it in GitHub Desktop.
Implementation of State-Action-Model pattern
class Machine {
constructor(spec) {
this.acceptors = spec.acceptors;
this.actions = spec.actions;
this.sr = spec.state;
this.model = spec.model;
}
// All model mutations happen through this function
present(proposals) {
proposals.forEach(proposal => {
const acceptor = this.acceptors[proposal.type];
acceptor(this.model, proposal.payload);
});
return this.state();
}
invokeAction(eventName, payload) {
const action = this.actions[eventName];
if (typeof action == "undefined") {
throw new Error(`Event ${eventName} has no associated action`);
}
if (typeof action.validate == "function") {
const errors = action.validate();
if (errors.length) {
throw new Error('Invalid')
}
}
return this.present( action(payload) );
}
// Job is to produce an immutable state representation from the model,
// and invoke "next action" predicates
// https://sam.js.org/#state
state() {
// A pure function of the model
const stateRepresentation = this.sr(this.model);
// Wire up the loop, to allow next actions to be proposed
if (!this.nextAction(stateRepresentation)) {
// proposeChanges(a,b,c)
// proposeChanges(b,c,d)
// proposeChanges(a,b,c)
// persist here
return stateRepresentation;
}
}
nextAction(state, changes) {
console.log("NAP called. Current state.");
console.log(state);
if (state.counter > 2) {
const actions = [{
type: 'resetCounter',
payload: {}
}];
return this.present(actions);
}
// No next actions to be run
return false;
}
}
// Just organising functions for "acceptors", "actions", and "state", along side the "model" data.
const machineDefinition = {
model: {
counter: 0,
lastChangedBy: null
},
// smallest units of change in the model
acceptors: {
resetCounter: (model, payload) => {
model.counter = 0;
},
incrementCounter: (model, payload) => {
model.counter = model.counter + payload.incrementBy;
},
setLastChangedBy: (model, payload) => {
model.lastChangedBy = payload.name;
}
},
// map intents to model changed using acceptors
actions: {
increment: intent => {
return [
{
type: 'incrementCounter',
payload: {
incrementBy: 1,
}
},
{
type: 'setLastChangedBy',
payload: {
name: "pete"
}
}];
},
},
state: model => ({
counter: model.counter,
changedBy: model.lastChangedBy,
someString: `${model.counter} - ${model.lastChangedBy}`
})
};
const myMachine = new Machine(machineDefinition);
myMachine.invokeAction("increment", { name: "pete"});
myMachine.invokeAction("increment", { name: "someone else"});
myMachine.invokeAction("increment", { name: "pete"});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment