Created
January 25, 2023 14:34
-
-
Save rob-gordon/7bc2aefcd6cc353e45cbe0c1f48351c5 to your computer and use it in GitHub Desktop.
Create a context-less xstate machine from a template literal
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
import { | |
AnyEventObject, | |
BaseActionObject, | |
StateNodeConfig, | |
TransitionConfig, | |
TransitionsConfig, | |
createMachine, | |
interpret, | |
} from "xstate"; | |
import { parse } from "graph-selector"; | |
const machine = fsm` | |
one [initial] | |
BANG: two | |
BOOP: three | |
BEEP: four | |
MEEP: (one) | |
BANG: (five) | |
BING: five [final] | |
`; | |
const service = interpret(machine).start(); | |
service.send("BANG"); | |
service.send("BEEP"); | |
service.send("MEEP"); | |
service.send("BING"); | |
console.log(`\n-----\ncurrent state: ${service.state.value}\n-----\n`); | |
/* | |
*/ | |
/** A template literal for creating context-less state machines */ | |
function fsm(text: TemplateStringsArray) { | |
return machineFromString(text[0]); | |
} | |
function machineFromString(text: string) { | |
return createMachine<{}>(graphWithoutContextToFSM(text)); | |
} | |
function graphWithoutContextToFSM(text: string) { | |
return graphToFSM(text, {}, {}); | |
} | |
function graphToFSM<T>( | |
text: string, | |
context: T, | |
actions: Record<string, TransitionConfig<T, AnyEventObject>["actions"]> | |
) { | |
const KEYS_TO_REMOVE = ["id", "label", "classes"]; | |
const { nodes, edges } = parse(text); | |
// Build States | |
let states: StateNodeConfig< | |
T, | |
any, | |
AnyEventObject, | |
BaseActionObject | |
>["states"] = {}; | |
// Store initial state | |
let initial: string = ""; | |
for (const node of nodes) { | |
states[node.data.label] = {}; | |
const currentState = states[node.data.label]; | |
// if has initial flag, add to intial | |
if (node.data["initial"]) initial = node.data.label; | |
const edgesFrom = edges.filter((edge: any) => edge.source === node.data.id); | |
for (const edge of edgesFrom) { | |
if (!("on" in currentState)) { | |
currentState.on = {}; | |
} | |
let on = currentState.on as TransitionsConfig<T, AnyEventObject>; | |
const target = | |
nodes.find((node: any) => node.data.id === edge.target)?.data.label || | |
""; | |
if (!target) continue; | |
// Check for an action (attribute on the edge, that isn't id, label, or classes) | |
// take the first one | |
const action = | |
Object.keys(edge.data).filter( | |
(key) => KEYS_TO_REMOVE.indexOf(key) === -1 | |
)?.[0] || ""; | |
const transition = edge.data.label; | |
if (action && !(action in actions)) | |
throw new Error(`Action ${action} not found`); | |
if (action) { | |
on[transition] = { | |
target, | |
action: actions[action], | |
}; | |
} else { | |
on[transition] = target; | |
} | |
} | |
if (node.data["final"]) { | |
currentState["final"] = true; | |
} | |
} | |
if (!initial) throw new Error("Missing initial state"); | |
const result = { | |
id: "my-machine", | |
initial, | |
context, | |
states, | |
}; | |
return result; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment