A process is made of nodes
Nodes are executables or actions
Actions are performed on the state
It then proceeds to the next node
The state is made of properties
The state may be given special system symbols containing execution information
Machines have multiple stages, refered to by strings or symbols
Sequences have multiple indexes, refered to by numbers
Conditions (including switch) have clauses
Process
const instance = new S({ result: 'value' })
return instance.process // { result: 'value' }
Config
const instance = new S()
return instance.config // { defaults: { result: null }, iterations: 10000, strict: false, async: false }
const instance = new S()
const modifiedInstance = instance
.async
.for(10)
.defaults({ result: 'other' })
.strict
return modifiedInstance.config // { defaults: { result: 'other' }, iterations: 10, strict: true, async: true }
The primary way of interacting with this library is to create a new instance
const instance = new S() // Succeeds
Instances are executable, like functions
const instance = new S()
return instance() === undefined // Succeeds
The constructor takes two arguments, the process
and the config
const instance = new S({}, {})
return instance() // Succeeds
Neither of these arguments are required, and it is not recommended to configure them config via the constructor. Instead you should update the config using the various chainable methods and properties.
const instance = new S(process)
.defaults({})
.input()
.result() // Succeeds
Create an ExtensibleFunction that can execute the run
or override
method in scope of the new SuperSmallStateMachine instance.
This is private so it cannot be mutated at runtime
The process must be public, it cannot be deep merged or cloned as it may contain symbols.
Returns the path of the closest ancestor to the node at the given path
that matches one of the given nodeTypes
.
Returns null
if no ancestor matches the one of the given nodeTypes
.
const instance = new S([
{
if: ({ result }) => result === 'start',
then: [
{ result: 'second' },
S.Return,
]
}
])
return instance.closest([0, 'then', 1], 'sequence') // [ 0, 'then' ]
Safely apply the given changes
to the given state
.
Merges the changes
with the given state
and returns it.
This will ignore any symbols in changes
, and forward the important symbols of the given state
.
const instance = new S()
const result = instance.changes({
[S.Path]: ['preserved'],
[S.Changes]: {},
preserved: 'value',
common: 'initial',
}, {
[S.Path]: ['ignored'],
[S.Changes]: { ignored: true },
common: 'changed',
})
return result // { common: 'changed', preserved: 'value', [S.Path]: [ 'preserved' ], [S.Changes]: { ignored: undefined, common: 'changed' } }
Proceed to the next execution path.
Performs fallback logic when a node exits.
Perform actions on the state.
Applies any changes in the given action
to the given state
.
Proceeds to the next node if the action is not itself a directive or return.
Execute a node in the process, return an action.
Executes the node in the process at the state's current path and returns it's action.
If the node is not executable it will be returned as the action.
TODO: traverse and adapt same thing?
Execute the entire process either synchronously or asynchronously depending on the config.
Execute the entire process synchronously.
Execute the entire process asynchronously. Always returns a promise.
Defines a process to execute, overrides the existing process.
Returns a new instance.
const instance = new S({ result: 'old' })
.do({ result: 'new' })
return instance() // 'new'
Defines the initial state to be used for all executions.
Returns a new instance.
const instance = new S()
.defaults({ result: 'default' })
return instance() // 'default'
Allows the definition of the arguments the executable will use, and how they will be applied to the initial state.
Returns a new instance.
const instance = new S(({ first, second }) => ({ [S.Return]: `${first} then ${second}` }))
.defaults({ first: '', second: '' })
.input((first, second) => ({ first, second }))
return instance('this', 'that') // 'this then that'
Allows the modification of the value the executable will return.
Returns a new instance.
const instance = new S(({ myReturnValue }) => ({ myReturnValue: myReturnValue + ' extra' }))
.result(state => state.myReturnValue)
return instance({ myReturnValue: 'start' }) // 'start extra'
Execute without checking state properties when a state change is made.
Creates a new instance.
const instance = new S(() => ({ unknownVariable: false}))
.defaults({ knownVariable: true })
.strict
return instance() // StateReferenceError
const instance = new S(() => ({ unknownVariable: false}))
.defaults({ knownVariable: true })
.strict
.unstrict
return instance() // Succeeds
Checks state properties when an state change is made.
Creates a new instance.
const instance = new S(() => ({ unknownVariable: false}))
.defaults({ knownVariable: true })
return instance() // Succeeds
const instance = new S(() => ({ unknownVariable: false}))
.defaults({ knownVariable: true })
.strict
return instance() // StateReferenceError
Checking state property types when an state change is made.
Creates a new instance.
const instance = new S([() => ({ knownVariable: 45 }), ({ knownVariable }) => ({ result: knownVariable })])
.defaults({ knownVariable: true })
.strictTypes
return instance() // StateTypeError
Defines the maximum iteration limit.
Returns a new instance.
const instance = new S([
({ result }) => ({ result: result + 1}),
0
])
.defaults({ result: 0 })
.for(10)
return instance() // MaxIterationsError
Stops execution of the machine once the given condition is met, and attempts to return.
const instance = new S([
({ result }) => ({ result: result + 1 }),
{
if: ({ result }) => result > 4,
then: [{ result: 'exit' }, { result:'ignored' }],
else: 0
}
])
.until(({ result }) => result === 'exit')
return instance({ result: 0 }) // 'exit'
Removes the max iteration limit.
Creates a new instance.
const instance = new S().forever
return instance.config.iterations // Infinity
Execute synchronously and not allow for asynchronous actions.
Creates a new instance.
const instance = new S(async () => ({ result: 'changed' }))
.async
.sync
.defaults({ result: 'initial' })
return instance() // 'initial'
Execute asynchronously and allow for asynchronous actions.
Creates a new instance.
const instance = new S(async () => ({ result: 'changed' }))
.defaults({ result: 'initial' })
return instance() // 'initial'
const instance = new S(async () => ({ result: 'changed' }))
.defaults({ result: 'initial' })
.async
return await instance() // 'changed'
Allows an async execution to be paused between steps.
Returns a new instance.
Overrides the method that will be used when the executable is called.
Returns a new instance.
const instance = new S({ result: 'definedResult' }).override(function (...args) {
// console.log({ scope: this, args }) // { scope: { process: { result: 'definedResult' } }, args: [1, 2, 3] }
return 'customReturn'
})
return instance(1, 2, 3) // 'customReturn'
Allows for the addition of new node types.
Returns a new instance.
const specialSymbol = Symbol('My Symbol')
class SpecialNode extends NodeDefinition {
static name = 'special'
static typeof(object, objectType) { return Boolean(objectType === 'object' && object && specialSymbol in object)}
static execute(){ return { [S.Return]: 'specialValue' } }
}
const instance = new S({ [specialSymbol]: true })
.addNode(SpecialNode)
return instance({ result: 'start' }) // 'specialValue'
const specialSymbol = Symbol('My Symbol')
const instance = new S({ [specialSymbol]: true })
return instance({ result: 'start' }) // 'start'
Transforms the process before usage, allowing for temporary nodes.
const replaceMe = Symbol('replace me')
const instance = new S([
replaceMe,
S.Return,
])
.adapt(function (process) {
return S.traverse((node) => {
if (node === replaceMe)
return { result: 'changed' }
return node
})(this) })
return instance({ result: 'unchanged' }) // 'changed'
Transforms the state before execution.
Returns a new instance.
const instance = new S()
.adaptStart(state => ({
...state,
result: 'overridden'
}))
return instance({ result: 'input' }) // 'overridden'
Transforms the state after execution.
Returns a new instance.
const instance = new S()
.adaptEnd(state => ({
...state,
result: 'overridden'
}))
return instance({ result: 'start' }) // 'overridden'
Allows for the addition of predifined modules.
Returns a new instance.
const instance = new S()
.with(S.strict, S.async, S.for(10))
return instance.config // { async: true, strict: true, iterations: 10 }
Returns the path of the closest ancestor to the node at the given path
that matches one of the given nodeTypes
.
Returns null
if no ancestor matches the one of the given nodeTypes
.
const instance = new S([
{
if: ({ result }) => result === 'start',
then: [
{ result: 'second' },
S.Return,
]
}
])
return S.closest([0, 'then', 1], 'sequence')(instance) // [ 0, 'then' ]
Safely apply the given changes
to the given state
.
Merges the changes
with the given state
and returns it.
This will ignore any symbols in changes
, and forward the important symbols of the given state
.
const instance = new S()
const result = S.changes({
[S.Path]: ['preserved'],
[S.Changes]: {},
preserved: 'value',
common: 'initial',
}, {
[S.Path]: ['ignored'],
[S.Changes]: { ignored: true },
common: 'changed',
})(instance)
return result // { common: 'changed', preserved: 'value', [S.Path]: [ 'preserved' ], [S.Changes]: { ignored: undefined, common: 'changed' } }
Proceed to the next execution path.
Performs fallback logic when a node exits.
Perform actions on the state.
Applies any changes in the given action
to the given state
.
Proceeds to the next node if the action is not itself a directive or return.
Execute a node in the process, return an action.
Executes the node in the process at the state's current path and returns it's action.
If the node is not executable it will be returned as the action.
TODO: traverse and adapt same thing?
Execute the entire process either synchronously or asynchronously depending on the config.
Execute the entire process synchronously.
Execute the entire process asynchronously. Always returns a promise.
Defines a process to execute, overrides the existing process.
Returns a function that will modify a given instance.
const instance = new S({ result: 'old' })
const newInstance = instance.with(S.do({ result: 'new' }))
return newInstance() // 'new'
Defines the initial state to be used for all executions.
Returns a function that will modify a given instance.
const instance = new S()
const newInstance = instance.with(S.defaults({ result: 'default' }))
return newInstance() // 'default'
Allows the definition of the arguments the executable will use, and how they will be applied to the initial state.
Returns a function that will modify a given instance.
const instance = new S(({ first, second }) => ({ [S.Return]: `${first} then ${second}` }))
.with(
S.defaults({ first: '', second: '' }),
S.input((first, second) => ({ first, second }))
)
return instance('this', 'that') // 'this then that'
Allows the modification of the value the executable will return.
Returns a function that will modify a given instance.
const instance = new S(({ myReturnValue }) => ({ myReturnValue: myReturnValue + ' extra' }))
.with(S.result(state => state.myReturnValue))
return instance({ myReturnValue: 'start' }) // 'start extra'
Execute without checking state properties when a state change is made.
Will modify the given instance.
With the strict flag, an unknown property cannot be set on the state.
const instance = new S(() => ({ unknownVariable: false}))
.with(
S.defaults({ knownVariable: true }),
S.strict
)
return instance() // StateReferenceError
The unstrict flag will override strict behaviour, so that an unknown property can be set on the state.
const instance = new S(() => ({ unknownVariable: false}))
.with(
S.defaults({ knownVariable: true }),
S.strict,
S.unstrict
)
return instance() // Succeeds
Checks state properties when an state change is made.
Will modify the given instance.
Without the strict flag, unknown properties can be set on the state by a state change action.
const instance = new S(() => ({ unknownVariable: false}))
.with(S.defaults({ knownVariable: true }))
return instance() // Succeeds
With the strict flag, unknown properties cannot be set on the state by a state change action.
const instance = new S(() => ({ unknownVariable: false}))
.with(
S.defaults({ knownVariable: true }),
S.strict
)
return instance() // StateReferenceError
Checking state property types when an state change is made.
Will modify the given instance.
With the strict types flag, known properties cannot have their type changed by a state change action
const instance = new S(() => ({ knownVariable: 45 }))
.with(
S.defaults({ knownVariable: true }),
S.strictTypes
)
return instance() // StateTypeError
Defines the maximum iteration limit.
Returns a function that will modify a given instance.
A limited number of iterations will cause the machine to exit early
const instance = new S([
({ result }) => ({ result: result + 1}),
0
])
.with(
S.defaults({ result: 0 }),
S.for(10)
)
return instance() // MaxIterationsError
Stops execution of the machine once the given condition is met, and attempts to return.
Returns a function that will modify a given instance.
const instance = new S([
({ result }) => ({ result: result + 1 }),
{
if: ({ result }) => result > 4,
then: [{ result: 'exit' }, { result:'ignored' }],
else: 0
}
])
.with(S.until(({ result }) => result === 'exit'))
return instance({ result: 0 }) // 'exit'
Removes the max iteration limit.
Will modify the given instance.
const instance = new S().with(S.forever)
return instance.config.iterations // Infinity
Execute synchronously and not allow for asynchronous actions.
Will modify the given instance.
const instance = new S(async () => ({ result: 'changed' }))
.with(
S.async,
S.sync,
S.defaults({ result: 'initial' })
)
return instance() // 'initial'
Execute asynchronously and allow for asynchronous actions.
Will modify the given instance.
const instance = new S(async () => ({ result: 'changed' }))
.with(S.defaults({ result: 'initial' }))
return instance() // 'initial'
const instance = new S(async () => ({ result: 'changed' }))
.with(
S.defaults({ result: 'initial' }),
S.async
)
return await instance() // 'changed'
Allows an async execution to be paused between steps.
Returns a function that will modify a given instance.
Overrides the method that will be used when the executable is called.
Returns a function that will modify a given instance.
const instance = new S({ result: 'definedResult' })
.with(S.override(function (...args) {
// console.log({ scope: this, args }) // { scope: { process: { result: 'definedResult' } }, args: [1, 2, 3] }
return 'customReturn'
}))
return instance(1, 2, 3) // 'customReturn'
Allows for the addition of new node types.
Returns a function that will modify a given instance.
const specialSymbol = Symbol('My Symbol')
class SpecialNode extends NodeDefinition {
static name = 'special'
static typeof(object, objectType) { return Boolean(objectType === 'object' && object && specialSymbol in object)}
static execute(){ return { [S.Return]: 'specialValue' } }
}
const instance = new S({ [specialSymbol]: true })
.with(S.addNode(SpecialNode))
return instance({ result: 'start' }) // 'specialValue'
const specialSymbol = Symbol('My Symbol')
const instance = new S({ [specialSymbol]: true })
return instance({ result: 'start' }) // 'start'
Transforms the process before usage, allowing for temporary nodes.
Returns a function that will modify a given instance.
const replaceMe = Symbol('replace me')
const instance = new S([
replaceMe,
S.Return,
]).with(S.adapt(function (process) {
return S.traverse((node) => {
if (node === replaceMe)
return { result: 'changed' }
return node
})(this) }))
return instance({ result: 'unchanged' }) // 'changed'
Transforms the state before execution.
Returns a function that will modify a given instance.
const instance = new S()
.adaptStart(state => ({
...state,
result: 'overridden'
}))
return instance({ result: 'input' }) // 'overridden'
Transforms the state after execution.
Returns a function that will modify a given instance.
const instance = new S()
.adaptEnd(state => ({
...state,
result: 'overridden'
}))
return instance({ result: 'input' }) // 'overridden'
Allows for the addition of predifined modules.
Returns a function that will modify a given instance.
const plugin = S.with(S.strict, S.async, S.for(10))
const instance = new S().with(plugin)
return instance.config // { async: true, strict: true, iterations: 10 }
Allow the input of a list or a list of lists, etc.
Pass each state through the adapters sequentially.
Make sure an instance is returned.
Every instance must have a process and be callable.
Use for intentionally exiting the entire process, can be used in an object to override the result value before returning
return { [S.Return]: "value" } // Succeeds
Returned in the state. Should not be passed in.
return { [S.Changes]: {} } // Succeeds
Returned in the state to indicate the next action path, or passed in with the state to direct the machine. This can also be used as a node on its own to change the executing path.
return { [S.Path]: [] } // Succeeds
Possible value of config.strict
, used to indicate strict types as well as values.
Key Words
Node Types
Initialise the result property as null
by default
Input the initial state by default
Return the result property by default
Do not perform strict state checking by default
Allow 1000 iterations by default
Run util the return symbol is present by default.
Do not allow for asynchronous actions by default
Do not allow for asynchronous actions by default
Do not override the execution method by default
Use the provided nodes by default.
Initialise with an empty adapters list.
Initialise with an empty start adapters list.
Initialise with an empty end adapters list.
Returns the path of the closest ancestor to the node at the given path
that matches one of the given nodeTypes
.
Returns null
if no ancestor matches the one of the given nodeTypes
.
const instance = new S([
{
if: ({ result }) => result === 'start',
then: [
{ result: 'second' },
S.Return,
]
}
])
return S._closest(instance, [0, 'then', 1], 'sequence') // [ 0, 'then' ]
Node types can be passed in as arrays of strings, or arrays of arrays of strings...
Get the type of the node
Pick this node if it matches any of the given types
Safely apply the given changes
to the given state
.
Merges the changes
with the given state
and returns it.
This will ignore any symbols in changes
, and forward the important symbols of the given state
.
const instance = new S()
const result = S._changes(instance, {
[S.Path]: ['preserved'],
[S.Changes]: {},
preserved: 'value',
common: 'initial',
}, {
[S.Path]: ['ignored'],
[S.Changes]: { ignored: true },
common: 'changed',
})
return result // { common: 'changed', preserved: 'value', [S.Path]: [ 'preserved' ], [S.Changes]: { ignored: undefined, common: 'changed' } }
Throw a StateReferenceError if a property is referenced that did not previosly exist.
Collect all the errors, using the same logic as above.
Throw a StateTypeError if a property changes types.
Collect all the changes in the changes object.
Deep merge the current state with the new changes
Carry over the original path.
Update the changes to the new changes
Proceed to the next execution path.
const instance = new S([
'firstAction',
'secondAction'
])
return S._proceed(instance, {
[S.Path]: [0]
}) // { [S.Path]: [ 1 ] }
Performs fallback logic when a node exits.
const instance = new S([
[
'firstAction',
'secondAction',
],
'thirdAction'
])
return S._proceed(instance, {
[S.Path]: [0,1]
}) // { [S.Path]: [ 1 ] }
Return null
(unsuccessful) if the root node is reached
Get the next closest ancestor that can be proceeded
If no such node exists, return null
(unsuccessful)
Get this closest ancestor
Determine what type of node the ancestor is
Get the node defintion for the ancestor
If the node definition cannot be proceeded, return null
(unsuccessful)
Call the proceed
method of the ancestor node to get the next path.
If there a next path, return it
Proceed updwards through the tree and try again.
Perform actions on the state.
const instance = new S([
'firstAction',
'secondAction',
'thirdAction'
])
return S._perform(instance, { [S.Path]: [0], prop: 'value' }, { prop: 'newValue' }) // { prop: 'newValue', [S.Path]: [ 1 ] }
Applies any changes in the given action
to the given state
.
const instance = new S([
'firstAction',
'secondAction',
'thirdAction'
])
return S._perform(instance, { [S.Path]: [0], prop: 'value' }, { [S.Path]: [2] }) // { prop: 'value', [S.Path]: [ 2 ] }
Proceeds to the next node if the action is not itself a directive or return.
const instance = new S([
'firstAction',
'secondAction'
])
return S._perform(instance, { [S.Path]: [0] }, null) // { [S.Path]: [ 1 ] }
Get the current path, default to the root node.
Get the node type of the given action
Gets the node definition for the action
Perform the action on the state
Throw a NodeTypeError if the action cannot be performed
Executes the node in the process at the state's current path and returns it's action.
const instance = new S([
() => ({ result: 'first' }),
() => ({ result: 'second' }),
() => ({ result: 'third' }),
])
return S._execute(instance, { [S.Path]: [1] }) // { result: 'second' }
If the node is not executable it will be returned as the action.
const instance = new S([
({ result: 'first' }),
({ result: 'second' }),
({ result: 'third' }),
])
return S._execute(instance, { [S.Path]: [1] }) // { result: 'second' }
Get the node at the given path
Get the type of that node
Get the definition of the node
Execute the node and return its resulting action
If it cannot be executed, return the node to be used as an action
Traverses a process, mapping each node to a new value, effectively cloning the process.
You can customise how each leaf node is mapped by supplying the iterator
method
You can also customise how branch nodes are mapped by supplying the post
method
The post method will be called after child nodes have been processed by the iterator
Make sure the post functions is scoped to the given instance
Get the node at the given path
Get the type of the node
Get the definition of the node
Traverse it
If it cannot be traversed, it is a leaf node
Call the primary method and return the result
Execute the entire process either synchronously or asynchronously depending on the config.
If the process is asynchronous, execute use runAsync
If the process is asynchronous, execute use runSync
Execute the entire process synchronously.
Extract the useful parts of the config
Turn the arguments into an initial condition
Default to an empty change object
Use the defaults as an initial state
Use the path from the initial state - allows for starting at arbitrary positions
This should be fine for most finite machines, but may be too little for some constantly running machines.
Do it first to catch starting with a S.Return
in place.
Throw new MaxIterationsError
Execute the current node on the process and perform any required actions. Updating the currentState
When returning, run the ends state adapters, then the result adapter to complete execution.
Execute the entire process asynchronously. Always returns a promise.
Extract the useful parts of the config
Turn the arguments into an initial condition
Default to an empty change object
Use the defaults as an initial state
Use the path from the initial state - allows for starting at arbitrary positions
This should be fine for most finite machines, but may be too little for some constantly running machines.
Pause execution based on the pause customisation method
Check the configured until
condition to see if we should exit.
Throw new MaxIterationsError
Execute the current node on the process and perform any required actions. Updating the currentState
When returning, run the ends state adapters, then the result adapter to complete execution.
Updates the state by deep-merging the properties. Arrays will not be deep merged.
Overrides existing properties when provided
const instance = new S({ result: 'overridden' })
return instance({ result: 'start' }) // 'overridden'
Adds new properties while preserving existing properties
const instance = new S({ result: { newValue: true } })
return instance({ result: { existingValue: true } }) // { existingValue: true, newValue: true }
This definition is exported by the library as { ChangesNode }
import { ChangesNode } from './index.js'
return ChangesNode; // success
Use the NodeTypes.CH
(changes) value as the name.
Any object not caught by other conditions should qualify as a state change.
Apply the changes to the state and step forward to the next node
Sequences are lists of nodes and executables, they will visit each node in order and exit when done.
Sequences will execute each index in order
const instance = new S([
({ result }) => ({ result: result + ' addition1' }),
({ result }) => ({ result: result + ' addition2' }),
])
return instance({ result: 'start' }) // 'start addition1 addition2'
This definition is exported by the library as { SequenceNode }
import { SequenceNode } from './index.js'
return SequenceNode; // success
Use the NodeTypes.SQ
(sequence) value as the name.
Get the sequence at the path
Get the current index in this sequence from the path
Increment the index, unless the end has been reached
A sequence is an array. A sequence cannot be an action, that will be interpreted as an absolute-directive.
Execute a sequence by directing to the first node (so long as it has nodes)
Traverse a sequence by iterating through each item in the array.
The only argument to the function will be the state.
You can return any of the previously mentioned action types from a function, or return nothing at all for a set-and-forget action.
A function can return a state change
const instance = new S(({ result }) => ({ result: result + ' addition' }))
return instance({ result: 'start' }) // 'start addition'
A function can return a directive
const instance = new S([
{ result: 'first' },
() => 4,
{ result: 'skipped' },
S.Return,
{ result: 'second' },
])
return instance({ result: 'start' }) // 'second'
A function can return a return statement
const instance = new S(() => ({ [S.Return]: 'changed' }))
return instance({ result: 'start' }) // 'changed'
A function can do anything without needing to return (set and forget)
const instance = new S(() => {
// Arbitrary code
})
return instance({ result: 'start' }) // 'start'
This definition is exported by the library as { FunctionNode }
import { FunctionNode } from './index.js'
return FunctionNode; // success
Use the NodeTypes.FN
(function) value as the name.
A function is a JS function. A function cannot be an action.
Exectute a functon by running it, passing in the state.
This definition is exported by the library as { UndefinedNode }
import { UndefinedNode } from './index.js'
return UndefinedNode; // success
Use the NodeTypes.UN
(undefined) value as the name.
Undefined is the undefined
keyword.
Un undefined node cannot be executed, throw an error to help catch incorrect configuration.
const instance = new S([undefined])
return instance() // UndefinedNodeError
When used as an action, undefined only moves to the next node.
const instance = new S([() => undefined, { result: 'second' }])
return instance({ result: 'start' }) // 'second'
This definition is exported by the library as { EmptyNode }
import { EmptyNode } from './index.js'
return EmptyNode; // success
Use the NodeTypes.EM
(empty) value as the name.
Empty is the null
keyword.
Empty is a no-op, and will do nothing except move to the next node
const instance = new S([null, { result: 'second' }, () => null])
return instance({ result: 'start' }) // 'second'
This definition is exported by the library as { ConditionNode }
import { ConditionNode } from './index.js'
return ConditionNode; // success
Use the NodeTypes.CD
(condition) value as the name.
A condition is an object with the 'if'
property. A condition cannot be an action.
Evaluate the 'if'
property as a function that depends on the state.
If truthy, direct to the 'then'
clause if it exists
const instance = new S({
if: ({ result }) => result === 'start',
then: { result: 'truthy' },
else: { result: 'falsey' },
})
return instance({ result: 'start' }) // 'truthy'
Otherwise, direct to the 'else'
clause if it exists
const instance = new S({
if: ({ result }) => result === 'start',
then: { result: 'truthy' },
else: { result: 'falsey' },
})
return instance({ result: 'other' }) // 'falsey'
Run post
on the result to allow the interception of the condition method.
Copy over the original properties to preserve any custom symbols.
Copy over the 'if'
property
Iterate on the 'then'
clause if it exists
Iterate on the 'else'
clause if it exists
const instance = new S({
switch: ({ result }) => result,
case: {
start: { result: 'first' },
two: { result: 'second' },
default: { result: 'none' },
}
})
const result1 = instance({ result: 'start' })
const result2 = instance({ result: 'two' })
const result3 = instance({ result: 'other' })
return { result1, result2, result3 } // { result1: 'first', result2: 'second', result3: 'none' }
This definition is exported by the library as { SwitchNode }
import { SwitchNode } from './index.js'
return SwitchNode; // success
Use the NodeTypes.SW
(switch) value as the name.
A switch node is an object with the 'switch'
property.
Execute a switch by evaluating the 'switch'
property and directing to the approprtate 'case'
clause.
Evaluate the 'switch'
property as a function that returns a key.
If the key exists in the 'case'
caluses, use the key, otherwise use the 'default'
clause
Check again if the key exists ('default'
clause may not be defined), if it does, redirect to the case, otherwise do nothing.
Copy over the original properties to preserve any custom symbols.
Copy over the 'switch'
property
Iterate over each of the 'case'
clauses.
const instance = new S({
initial: [
() => ({ result: 'first' }),
'next',
],
next: { result: 'second' }
})
return instance({ result: 'start' }) // 'second'
This definition is exported by the library as { MachineNode }
import { MachineNode } from './index.js'
return MachineNode; // success
Use the NodeTypes.MC
(machine) value as the name.
A machine is an object with the 'initial'
property. A machine cannot be used as an action.
Execute a machine by directing to the 'initial'
stages.
Copy over the original properties to preserve any custom symbols.
Iterate over each of the stages.
Transitioning is also possible by using and object with the S.Path
key set to a relative or absolute path. This is not recommended as it is almost never required, it should be considered system-only.
const instance = new S({
initial: [
{ result: 'first' },
{ [S.Path]: 'next' }
],
next: { result: 'second' }
})
return instance({ result: 'start' }) // 'second'
It is not possible to send any other information in this object, such as a state change.
const instance = new S({
initial: [
{ result: 'first' },
{ [S.Path]: 'next', result: 'ignored' }
],
next: S.Return
})
return instance({ result: 'start' }) // 'first'
This definition is exported by the library as { DirectiveNode }
import { DirectiveNode } from './index.js'
return DirectiveNode; // success
Use the NodeTypes.DR
(directive) value as the name.
A directive is an object with the S.Path
property.
A directive is performed by performing the value of the S.Path
property to allow for using absolute or relative directives
Numbers indicate a goto for a sequence. It is not recommended to use this as it may be unclear, but it must be possible, and should be considered system-only.
const instance = new S([
{ result: 'first' },
4,
{ result: 'skip' },
S.Return,
{ result: 'second' },
])
return instance({ result: 'start' }) // 'second'
Slightly less not recommended is transitioning in a sequence conditonally. If you're making an incredibly basic state machine this is acceptable.
const instance = new S([
{
if: ({ result }) => result === 'start',
then: 3,
else: 1,
},
{ result: 'skip' },
S.Return,
{ result: 'second' },
])
return instance({ result: 'start' }) // 'second'
This definition is exported by the library as { SequenceDirectiveNode }
import { SequenceDirectiveNode } from './index.js'
return SequenceDirectiveNode; // success
Use the NodeTypes.SD
(sequence-directive) value as the name.
A sequence directive is a number.
A sequence directive is performed by finding the last sequence and setting the index to the given value.
Get the closest ancestor that is a sequence.
If there is no such ancestor, throw a PathReferenceError
Update the path to the parent>index
Directives are effectively goto
commands, or transitions
if you prefer.
Directives are the natural way of proceeding in state machines, using the name of a neighboring state as a string you can direct flow through a state machine.
const instance = new S({
initial: [
{ result: 'first' },
'next'
],
next: { result: 'second' }
})
return instance({ result: 'start' }) // 'second'
You can also use symbols as state names.
const myState = Symbol('MyState')
const instance = new S({
initial: [
{ result: 'first' },
myState
],
[myState]: { result: 'second' }
})
return instance({ result: 'start' }) // 'second'
This definition is exported by the library as { MachineDirectiveNode }
import { MachineDirectiveNode } from './index.js'
return MachineDirectiveNode; // success
Use the NodeTypes.MD
(machine-directive) value as the name.
A machine directive is a string or a symbol.
Get the closest ancestor that is a machine.
If no machine ancestor is foun, throw a PathReferenceError
Update the path to parent>stage
Arrays can be used to perform absolute redirects. This is not recommended as it may make your transition logic unclear.
Arrays cannot be used on their own, because they would be interpreted as sequences. For this reason they must be contained in an object with the S.Path
symbol as a ky, with the array as the value, or returned by an action.
Using an absolute directive in a directive object works
const instance = new S({
initial: [
{ result: 'first' },
{ [S.Path]: ['next',1] }
],
next: [
{ result: 'skipped' },
S.Return,
]
})
return instance({ result: 'start' }) // 'first'
Using an absolute directive as a return value works
const instance = new S({
initial: [
{ result: 'first' },
() => ['next',1]
],
next: [
{ result: 'skipped' },
S.Return,
]
})
return instance({ result: 'start' }) // 'first'
Using an absolute directive as an action does NOT work.
const instance = new S({
initial: [
{ result: 'first' },
['next',1]
],
next: [
{ result: 'not skipped' },
S.Return,
]
})
return instance({ result: 'start' }) // 'not skipped'
This definition is exported by the library as { AbsoluteDirectiveNode }
import { AbsoluteDirectiveNode } from './index.js'
return AbsoluteDirectiveNode; // success
Use the NodeTypes.AD
(absolute-directive) value as the name.
An absolute directive is a list of strings, symbols, and numbers. It can only be used as an action as it would otherwise be interpreted as a sequence.
An absolute directive is performed by setting S.Path
to the path
Causes the entire process to terminate immediately and return, setting S.Return
to true
on the state.
If the symbol is used on its own, the it will simply return whatever value is in the "result".
It is reccomended you use the result variable for this purpose.
const instance = new S(S.Return)
return instance({ result: 'start' }) // 'start'
Using the return symbol as the key to an object will override the result variable with that value before returning.
const instance = new S({ [S.Return]: 'custom' })
return instance({ result: 'start' }) // 'custom'
const instance = new S({ [S.Return]: 'custom' })
return instance.result(state => state)({ result: 'start' }) // { result: 'custom' }
This definition is exported by the library as { ReturnNode }
import { ReturnNode } from './index.js'
return ReturnNode; // success
Use the NodeTypes.RT
(return) value as the name.
A return node is the S.Return
symbol itself, or an object with an S.Return
property.
Perform a return by setting the result to the return value and setting the S.Return
flag on the state to true
Copy the original properties from the state
Set S.Return
to true
Copy over the original path to preserve it.
Update the result if one was passed in as the return value.
This list is exported by the library as { nodes }
import { nodes } from './index.js'
return nodes; // success
All Super Small State Machine Errors will inherit from this class.
Allows for contextual information to be provided with the error
This class is exported by the library as { SuperSmallStateMachineError }
import { SuperSmallStateMachineError } from './index.js'
return SuperSmallStateMachineError; // success
Declare contextual properties on the class
Create a normal error with the message
Assign the given properties to the instance
All Super Small State Machine Reference Errors will inherit from this class
This class is exported by the library as { SuperSmallStateMachineReferenceError }
import { SuperSmallStateMachineReferenceError } from './index.js'
return SuperSmallStateMachineReferenceError; // success
All Super Small State Machine Type Errors will inherit from this class
This class is exported by the library as { SuperSmallStateMachineTypeError }
import { SuperSmallStateMachineTypeError } from './index.js'
return SuperSmallStateMachineTypeError; // success
A state change has set a property that was not defined in the original state defaults.
This is likely intentional, as this is not default behaviour.
This class is exported by the library as { StateReferenceError }
import { StateReferenceError } from './index.js'
return StateReferenceError; // success
A state change has updated a property that was defined as a different type in the original state defaults.
This is likely intentional, as this is not default behaviour.
This class is exported by the library as { StateTypeError }
import { StateTypeError } from './index.js'
return StateTypeError; // success
A node of an unknown type was used in a process.
This was probably caused by a custom node definition
This class is exported by the library as { NodeTypeError }
import { NodeTypeError } from './index.js'
return NodeTypeError; // success
An undefined node was used in a process.
This is probably caused by a missing variable.
If you wish to perform an intentional no-op, use null
This class is exported by the library as { UndefinedNodeError }
import { UndefinedNodeError } from './index.js'
return UndefinedNodeError; // success
The execution of the process took more iterations than was allowed.
This can be configured using .for
or .forever
This class is exported by the library as { MaxIterationsError }
import { MaxIterationsError } from './index.js'
return MaxIterationsError; // success
A path was referenced which could not be found in the given process.
This class is exported by the library as { PathReferenceError }
import { PathReferenceError } from './index.js'
return PathReferenceError; // success