Created
September 6, 2021 16:04
-
-
Save petebarnett/d7125b0951a1205e92738f1489bab749 to your computer and use it in GitHub Desktop.
Implementation of State-Action-Model pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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