A process is made of nodes
Nodes are executables or actions
There are three phases: execute, perform, proceed
-
An executable results in an action
-
Actions are performed on the state
-
Then we proceed 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
Machines have multiple interrupts, refered to by symbols
Sequences have multiple indexes, refered to by numbers
Conditions (including switch) have clauses
To create a new state machine, create a new instance of the S
class
const instance = new S() // Succeeds
The instance is executable, and can be run just like a function
const instance = new S([
{ myProperty: 'myValue' },
({ myProperty }) => ({ [Return]: myProperty })
])
return instance() // 'myValue'
The initial state can be passed into the function call
const instance = new S([
({ myProperty }) => ({ [Return]: myProperty })
])
return instance({ myProperty: 'myValue' }) // 'myValue'
An intuitive syntax can be used to construct the process of the state machine
const instance = new S({
initial: [
{ order: [] }, // Set the "order" property to an empty list
'second', // Goto the "second" stage
],
second: { // Conditionally add the next number
if: ({ order }) => order.length < 10,
then: ({ order }) => ({ order: [ ...order, order.length ] }),
else: 'end' // Goto the "end" stage if we have already counted to 10
},
end: ({ order }) => ({ [Return]: order }) // Return the list we have constructed
}) // Succeeds
To configure the state machine, you can cahin configuration methods
const instance = new S()
.deep
.strict
.forever // Succeeds
You can avoid making a new instance for each method by using .with
const specificConfig = S.with(S.deep, S.strict, S.forever)
const instance = new S()
.with(specificConfig) // Succeeds
Process
const instance = new S({ result: 'value' })
return instance.process // { result: 'value' }
Config
const instance = new S()
return instance.config // { defaults: { result: undefined }, iterations: 10000, strict: false }
const instance = new S()
const modifiedInstance = instance
.with(asyncPlugin)
.for(10)
.defaults({ result: 'other' })
.strict
return modifiedInstance.config // { defaults: { result: 'other' }, iterations: 10, strict: 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 via the constructor. Instead you should update the config using the various chainable methods and properties.
const instance = new S(process)
.defaults({})
.input()
.output() // Succeeds
It will execute the run
or override
method in scope of the new SuperSmallStateMachine instance.
This is private so it cannot be mutated at runtime
const myConfig = { iterations: 1000 }
const instance = new S(null, myConfig)
const retrievedConfig = instance.config
return retrievedConfig !== myConfig && retrievedConfig !== instance.config // true
const myConfig = { iterations: 'original' }
const instance = new S(null, myConfig)
instance.config.iterations = 'new value'
return instance.config.iterations // 'original'
The process must be public, it cannot be deep merged or cloned as it may contain symbols.
const myProcess = { mySpecialKey: 23864 }
const instance = new S(myProcess)
return instance.process === myProcess // true
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' },
Return,
]
}
])
return instance.closest([0, 'then', 1], SequenceNode.type) // [ 0, 'then' ]
Safely apply the given changes
to the given state
.
Merges the changes
with the given state
and returns it.
const instance = new S()
const result = instance.changes({
[Changes]: {},
preserved: 'value',
common: 'initial',
}, {
common: 'changed',
})
return result // { common: 'changed', preserved: 'value', [Changes]: { common: 'changed' } }
Proceed to the next execution path.
Performs fallback logic when a node exits.
const instance = new S([
null,
null,
[
null,
null,
],
null
])
return instance.proceed({ [Stack]: [{path:[ 2, 1 ],origin:Return,point:2}] }) // { [Stack]: [ { path: [ 3 ], origin: Symbol(SSSM Return), point: 1 } ] }
Perform actions on the state.
Applies any changes in the given action
to the given state
.
const instance = new S()
return instance.perform({ myProperty: 'start value' }, { myProperty: 'new value' }) // { myProperty: 'new value' }
Execute a node in the process, return an action.
Executes the node in the process at the state's current path and returns its action.
If the node is not executable it will be returned as the action.
const instance = new S([
{ myProperty: 'this value' },
{ myProperty: 'that value' },
{ myProperty: 'the other value' },
])
return instance.execute({ [Stack]: [{path:[1],origin:Return,point:1}], myProperty: 'start value' }, get_path_object(instance.process, [1])) // { myProperty: 'that value' }
Traverses the process of the instance, 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
const instance = new S({
initial: 'swap this',
other: [
{
if: 'swap this too',
then: 'also swap this'
}
]
})
return instance.traverse((node, path, process, nodeType) => {
if (node === 'swap this') return 'with this'
if (node === 'also swap this') return 'with that'
if (nodeType === ConditionNode.type && node.if === 'swap this too')
return {
...node,
if: 'with another thing'
}
return node
}) // { initial: 'with this', other: [ { if: 'with another thing', then: 'with that' } ] }
Execute the entire process.
Will execute the process
const instance = new S({ [Return]: 'return value' })
return instance.run() // 'return value'
Will not handle promises in async mode even if it is configured
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
.with(asyncPlugin)
return instance.run() // undefined
Will not handle promises in sync mode
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
return instance.run() // undefined
Is the same as running the executable instance itself
const instance = new S({ [Return]: 'return value' })
return instance.run() === instance() // true
Takes the same arguments as the executable instance itself
const instance = new S(({ a, b, c }) => ({ [Return]: `${a} + ${b} - ${c}` }))
.input((a, b, c) => ({ a, b, c }))
return instance.run(1, 2, 3) === instance(1, 2, 3) // true
Defines a process to execute, overrides the existing process.
Returns a new instance.
const instance = new S({ [Return]: 'old' })
.do({ [Return]: 'new' })
return instance() // 'new'
Defines the initial state to be used for all executions.
Returns a new instance.
const instance = new S(({ result }) => ({ [Return]: result }))
.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 }) => ({ [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' }))
.output(state => state.myReturnValue)
return instance({ myReturnValue: 'start' }) // 'start extra'
Disables the stack trace.
Creates a new instance.
const instance = new S({
initial: 'other',
other: 'oneMore',
oneMore: [
null,
null
]
}).untrace
.output(({ [Trace]: trace }) => trace)
return instance() // [ ]
Enables the stack trace.
Creates a new instance.
const instance = new S({
initial: 'other',
other: 'oneMore',
oneMore: [
null,
null
]
}).trace
.output(({ [Trace]: trace }) => trace)
return instance() // [ [ { path: [ ], origin: Symbol(SSSM Return), point: 0 } ], [ { path: [ 'initial' ], origin: Symbol(SSSM Return), point: 1 } ], [ { path: [ 'other' ], origin: Symbol(SSSM Return), point: 1 } ], [ { path: [ 'oneMore' ], origin: Symbol(SSSM Return), point: 1 } ], [ { path: [ 'oneMore', 0 ], origin: Symbol(SSSM Return), point: 2 } ], [ { path: [ 'oneMore', 1 ], origin: Symbol(SSSM Return), point: 2 } ] ]
Shallow merges the state every time a state change is made.
Creates a new instance.
const instance = new S({ myProperty: { existingKey: 'newValue', deepKey: { deepValue2: 7 } } })
.shallow
.output(ident)
return instance({ myProperty: { existingKey: 'existingValue', anotherKey: 'anotherValue', deepKey: { deepVaue: 6 } } }) // { myProperty: { existingKey: 'newValue', anotherKey: undefined, deepKey: { deepVaue: undefined, deepValue2: 7 } } }
Deep merges the all properties in the state every time a state change is made.
Creates a new instance.
const instance = new S({ myProperty: { existingKey: 'newValue', deepKey: { deepValue2: 7 } } })
.deep
.output(ident)
return instance({ myProperty: { existingKey: 'existingValue', anotherKey: 'anotherValue', deepKey: { deepVaue: 6 } } }) // { myProperty: { existingKey: 'newValue', anotherKey: 'anotherValue', deepKey: { deepVaue: 6, deepValue2: 7 } } }
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 }) => ({ [Return]: 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
}
])
.output(({ result }) => result)
.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
Overrides the method that will be used when the executable is called.
Returns a new instance.
const instance = new S({ [Return]: 'definedResult' })
.override(function (a, b, c) {
// console.log({ scope: this, args }) // { scope: { process: { result: 'definedResult' } }, args: [1, 2, 3] }
return `customResult. a: ${a}, b: ${b}, c: ${c}`
})
return instance(1, 2, 3) // 'customResult. a: 1, b: 2, c: 3'
Allows for the addition of new node types.
Returns a new instance.
const specialSymbol = Symbol('My Symbol')
class SpecialNode extends Node {
static type = 'special'
static typeof(object, objectType) { return Boolean(objectType === 'object' && object && specialSymbol in object)}
static execute(){ return { [Return]: 'specialValue' } }
}
const instance = new S({ [specialSymbol]: true })
.output(({ result, [Return]: output = result }) => output)
.addNode(SpecialNode)
return instance({ result: 'start' }) // 'specialValue'
const specialSymbol = Symbol('My Symbol')
const instance = new S({ [specialSymbol]: true })
.output(({ result, [Return]: output = result }) => output)
return instance({ result: 'start' }) // 'start'
Transforms the process before usage, allowing for temporary nodes.
const replaceMe = Symbol('replace me')
const instance = new S([
replaceMe,
]).adapt(function (process) {
return S.traverse((node) => {
if (node === replaceMe)
return { [Return]: 'replaced' }
return node
})(this)
})
return instance() // 'replaced'
Transforms the state before execution.
Returns a new instance.
const instance = new S()
.output(({ result }) => result)
.before(state => ({
...state,
result: 'overridden'
}))
return instance({ result: 'input' }) // 'overridden'
Transforms the state after execution.
Returns a new instance.
const instance = new S()
.output(({ result }) => result)
.after(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, asyncPlugin, S.for(10))
return instance.config // { strict: true, iterations: 10 }
The main class is exported as { StateMachine }
import { StateMachine } from './index.js'
return StateMachine; // success
The main class is exported as { SuperSmallStateMachine }
import { SuperSmallStateMachine } from './index.js'
return SuperSmallStateMachine; // success
The node class is exported as { NodeDefinition }
import { NodeDefinition } from './index.js'
return NodeDefinition; // success
The node collection class is exported as { NodeDefinitions }
import { NodeDefinitions } from './index.js'
return NodeDefinitions; // success
The node class is exported as { Node }
import { Node } from './index.js'
return Node; // success
The node collection class is exported as { Nodes }
import { Nodes } from './index.js'
return Nodes; // success
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' },
Return,
]
}
])
return S.closest([0, 'then', 1], SequenceNode.type)(instance) // [ 0, 'then' ]
Safely apply the given changes
to the given state
.
Merges the changes
with the given state
and returns it.
const instance = new S()
const result = S.changes({
[Changes]: {},
preserved: 'value',
common: 'initial',
}, {
common: 'changed',
})(instance)
return result // { common: 'changed', preserved: 'value', [Changes]: { common: 'changed' } }
Proceed to the next execution path.
Performs fallback logic when a node exits.
const instance = new S([
null,
null,
[
null,
null,
],
null
])
const proceeder = S.proceed({ [Stack]: [{path:[ 2, 1 ],origin:Return,point:2}] })
return proceeder(instance) // { [Stack]: [ { path: [ 3 ], origin: Symbol(SSSM Return), point: 1 } ] }
Perform actions on the state.
Applies any changes in the given action
to the given state
.
const instance = new S()
const performer = S.perform({ myProperty: 'start value' }, { myProperty: 'new value' })
return performer(instance) // { myProperty: 'new value' }
Execute a node in the process, return an action.
Executes the node in the process at the state's current path and returns its action.
If the node is not executable it will be returned as the action.
const instance = new S([
{ myProperty: 'this value' },
{ myProperty: 'that value' },
{ myProperty: 'the other value' },
])
const executor = S.execute({ [Stack]: [{path:[1],origin:Return,point:1}], myProperty: 'start value' })
return executor(instance) // { myProperty: 'that value' }
Traverses the process of the given instance, 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
const instance = new S({
initial: 'swap this',
other: [
{
if: 'swap this too',
then: 'also swap this'
}
]
})
const traverser = S.traverse((node, path, process, nodeType) => {
if (node === 'swap this') return 'with this'
if (node === 'also swap this') return 'with that'
if (nodeType === ConditionNode.type && node.if === 'swap this too')
return {
...node,
if: 'with another thing'
}
return node
})
return traverser(instance) // { initial: 'with this', other: [ { if: 'with another thing', then: 'with that' } ] }
Execute the entire process.
Will execute the process
const instance = new S({ [Return]: 'return value' })
return S.run()(instance) // 'return value'
Will not handle promises in async mode even if it is configured
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
.with(asyncPlugin)
return S.run()(instance) // undefined
Will not handle promises in sync mode
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
return S.run()(instance) // undefined
Is the same as running the executable instance itself
const instance = new S({ [Return]: 'return value' })
return S.run()(instance) === instance() // true
Takes the same arguments as the executable instance itself
const instance = new S(({ a, b, c }) => ({ [Return]: `${a} + ${b} - ${c}` }))
.input((a, b, c) => ({ a, b, c }))
return S.run(1, 2, 3)(instance) === instance(1, 2, 3) // true
Defines a process to execute, overrides the existing process.
Returns a function that will modify a given instance.
const instance = new S({ [Return]: 'old' })
const newInstance = instance.with(S.do({ [Return]: '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(({ result }) => ({ [Return]: result }))
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 }) => ({ [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.output(state => state.myReturnValue))
return instance({ myReturnValue: 'start' }) // 'start extra'
Shallow merges the state every time a state change is made.
Returns a function that will modify a given instance.
const instance = new S({
initial: 'other',
other: 'oneMore',
oneMore: [
null,
null
]
})
.with(S.untrace)
.output(({ [Trace]: trace }) => trace)
return instance() // [ ]
Deep merges the all properties in the state every time a state change is made.
Returns a function that will modify a given instance.
const instance = new S({
initial: 'other',
other: 'oneMore',
oneMore: [
null,
null
]
})
.with(S.trace)
.output(({ [Trace]: trace }) => trace)
return instance() // [ [ { path: [ ], origin: Symbol(SSSM Return), point: 0 } ], [ { path: [ 'initial' ], origin: Symbol(SSSM Return), point: 1 } ], [ { path: [ 'other' ], origin: Symbol(SSSM Return), point: 1 } ], [ { path: [ 'oneMore' ], origin: Symbol(SSSM Return), point: 1 } ], [ { path: [ 'oneMore', 0 ], origin: Symbol(SSSM Return), point: 2 } ], [ { path: [ 'oneMore', 1 ], origin: Symbol(SSSM Return), point: 2 } ] ]
Shallow merges the state every time a state change is made.
Returns a function that will modify a given instance.
const instance = new S({ myProperty: { existingKey: 'newValue', deepKey: { deepValue2: 7 } } })
.with(S.shallow)
.output(ident)
return instance({ myProperty: { existingKey: 'existingValue', anotherKey: 'anotherValue', deepKey: { deepVaue: 6 } } }) // { myProperty: { existingKey: 'newValue', anotherKey: undefined, deepKey: { deepVaue: undefined, deepValue2: 7 } } }
Deep merges the all properties in the state every time a state change is made.
Returns a function that will modify a given instance.
const instance = new S({ myProperty: { existingKey: 'newValue', deepKey: { deepValue2: 7 } } })
.with(S.deep)
.output(ident)
return instance({ myProperty: { existingKey: 'existingValue', anotherKey: 'anotherValue', deepKey: { deepVaue: 6 } } }) // { myProperty: { existingKey: 'newValue', anotherKey: 'anotherValue', deepKey: { deepVaue: 6, deepValue2: 7 } } }
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.output(({ result }) => result),
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
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({ [Return]: 'definedResult' })
.with(
S.override(function (a, b, c) {
// console.log({ scope: this, args }) // { scope: { process: { result: 'definedResult' } }, args: [1, 2, 3] }
return `customResult. a: ${a}, b: ${b}, c: ${c}`
})
)
return instance(1, 2, 3) // 'customResult. a: 1, b: 2, c: 3'
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 Node {
static type = 'special'
static typeof(object, objectType) { return Boolean(objectType === 'object' && object && specialSymbol in object)}
static execute(){ return { [Return]: 'specialValue' } }
}
const instance = new S({ [specialSymbol]: true })
.with(
S.output(({ result, [Return]: output = result }) => output),
S.addNode(SpecialNode)
)
return instance({ result: 'start' }) // 'specialValue'
const specialSymbol = Symbol('My Symbol')
const instance = new S({ [specialSymbol]: true })
.with(
S.output(({ result, [Return]: output = result }) => output)
)
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,
]).with(
S.adapt(function (process) {
return S.traverse((node) => {
if (node === replaceMe)
return { [Return]: 'replaced' }
return node
})(this)
})
)
return instance() // 'replaced'
Transforms the state before execution.
Returns a function that will modify a given instance.
const instance = new S()
.with(
S.output(({ result }) => result),
S.before(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()
.with(
S.output(({ result }) => result),
S.after(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, asyncPlugin, S.for(10))
const instance = new S().with(plugin)
return instance.config // { 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.
return S.config // { deep: false, strict: false, trace: false, iterations: 10000, override: null, adapt: [ ], before: [ ], after: [ ], defaults: { } }
Initialise an empty state by default
return Object.keys(new S(null).config.defaults) // [ ]
Input the initial state by default
return new S(null).config.input({ myProperty: 'myValue' }, 2, 3) // { myProperty: 'myValue' }
Return the Return
property by default
return new S(null).config.output({ [Return]: 'myValue' }) // 'myValue'
Do not perform strict state checking by default
return new S(null).config.strict // false
Allow 10000 iterations by default
return new S(null).config.iterations // 10000
Run until the return symbol is present by default.
return new S(null).config.until({ [Return]: undefined }) // true
Do not keep the stack trace by default
return new S(null).config.trace // false
Shallow merge changes by default
return new S(null).config.deep // false
Do not override the execution method by default
return new S(null).config.override // null
Uses the provided nodes by default.
return new S(null).config.nodes // { [Changes]: class ChangesNode extends Node {
static type = Changes
static typeof(object, objectType) { return Boolean(object && objectType === 'object') }
static perform(action, state) { return S._changes(this, state, action) }
}, [Sequence]: class SequenceNode extends Node {
static type = Sequence
static proceed(node, state) {
const index = state[Stack][0].path[state[Stack][0].point]
if (node && (typeof index === 'number') && (index+1 < node.length))
return { ...state, [Stack]: [{ ...state[Stack][0], path: [...state[Stack][0].path.slice(0,state[Stack][0].point), index+1], point: state[Stack][0].point + 1 }, ...state[Stack].slice(1)] }
return Node.proceed.call(this, node, state)
}
static typeof(object, objectType, isAction) { return ((!isAction) && objectType === 'object' && Array.isArray(object)) }
static execute(node, state) { return node.length ? [ ...state[Stack][0].path.slice(0,state[Stack][0].point), 0 ] : null }
static traverse(node, path, iterate) { return node.map((_,i) => iterate([...path,i])) }
}, [FunctionN]: class FunctionNode extends Node {
static type = FunctionN
static typeof(object, objectType, isAction) { return (!isAction) && objectType === 'function' }
static execute(node, state) { return node(state) }
}, [Condition]: class ConditionNode extends Node {
static type = Condition
static typeof(object, objectType, isAction) { return Boolean((!isAction) && object && objectType === 'object' && ('if' in object)) }
static keywords = ['if','then','else']
static execute(node, state) {
if (normalise_function(node.if)(state))
return 'then' in node ? [ ...state[Stack][0].path.slice(0,state[Stack][0].point), 'then' ] : null
return 'else' in node ? [ ...state[Stack][0].path.slice(0,state[Stack][0].point), 'else' ] : null
}
static traverse(node, path, iterate) { return {
...node,
...('then' in node ? { then: iterate([...path,'then']) } : {}),
...('else' in node ? { else: iterate([...path,'else']) } : {}),
...(Symbols in node ? Object.fromEntries(node[Symbols].map(key => [key, iterate([...path,key])])) : {}),
} }
}, [Switch]: class SwitchNode extends Node {
static type = Switch
static typeof(object, objectType, isAction) { return Boolean((!isAction) && object && objectType === 'object' && ('switch' in object)) }
static keywords = ['switch','case','default']
static execute(node, state) {
const key = normalise_function(node.switch)(state)
const fallbackKey = (key in node.case) ? key : 'default'
return (fallbackKey in node.case) ? [ ...state[Stack][0].path.slice(0,state[Stack][0].point), 'case', fallbackKey ] : null
}
static traverse(node, path, iterate) { return { ...node, case: Object.fromEntries(Object.keys(node.case).map(key => [ key, iterate([...path,'case',key]) ])), ...(Symbols in node ? Object.fromEntries(node[Symbols].map(key => [key, iterate([...path,key])])) : {}) } }
}, [While]: class WhileNode extends Node {
static type = While
static typeof(object, objectType, isAction) { return Boolean((!isAction) && object && objectType === 'object' && ('while' in object)) }
static keywords = ['while','do']
static execute(node, state) {
if (!(('do' in node) && normalise_function(node.while)(state))) return null
return [ ...state[Stack][0].path.slice(0,state[Stack][0].point), 'do' ]
}
static proceed(node, state) { return { ...state, [Stack]: [ { ...state[Stack][0], path: state[Stack][0].path.slice(0,state[Stack][0].point) }, ...state[Stack].slice(1) ] } }
static traverse(node, path, iterate) { return { ...node, ...('do' in node ? { do: iterate([ ...path, 'do' ]) } : {}), ...(Symbols in node ? Object.fromEntries(node[Symbols].map(key => [key, iterate([...path,key])])) : {}), } }
}, [Machine]: class MachineNode extends Node {
static type = Machine
static typeof(object, objectType, isAction) { return Boolean((!isAction) && object && objectType === 'object' && ('initial' in object)) }
static keywords = ['initial']
static execute(node, state) { return [ ...state[Stack][0].path.slice(0,state[Stack][0].point), 'initial' ] }
static traverse(node, path, iterate) { return { ...node, ...Object.fromEntries(Object.keys(node).concat(Symbols in node ? node[Symbols]: []).map(key => [ key, iterate([...path,key]) ])) } }
}, [Goto]: class GotoNode extends Node {
static type = Goto
static typeof(object, objectType, isAction) { return Boolean(object && objectType === 'object' && (Goto in object)) }
static perform(action, state) { return S._perform(this, state, action[Goto]) }
static proceed(node, state) { return state }
}, [InterruptGoto]: class InterruptGotoNode extends GotoNode {
static type = InterruptGoto
static typeof(object, objectType, isAction) { return objectType === 'symbol' }
static perform(action, state) {
const lastOf = get_closest_path(this.process, state[Stack][state[Stack].length-1].path.slice(0,state[Stack][state[Stack].length-1].point-1), parentNode => Boolean(parentNode && (typeof parentNode === 'object') && (action in parentNode)))
if (!lastOf) return { ...state, [Return]: action }
return { ...state, [Stack]: [ { origin: action, path: [...lastOf, action], point: lastOf.length + 1 }, ...state[Stack] ] }
}
static proceed(node, state) {
if (state[Return] === node) return ReturnNode.proceed.call(this, undefined, state)
const { [Stack]: stack, [Return]: interruptReturn, ...proceedPrevious } = S._proceed(this, { ...state, [Stack]: state[Stack].slice(1) }, undefined)
return { ...proceedPrevious, [Stack]: [ state[Stack][0], ...stack ] }
}
}, [AbsoluteGoto]: class AbsoluteGotoNode extends GotoNode {
static type = AbsoluteGoto
static typeof(object, objectType, isAction) { return isAction && Array.isArray(object) }
static perform(action, state) { return { ...state, [Stack]: [ { ...state[Stack][0], path: action, point: action.length }, ...state[Stack].slice(1) ] } }
}, [MachineGoto]: class MachineGotoNode extends GotoNode {
static type = MachineGoto
static typeof(object, objectType, isAction) { return objectType === 'string' }
static perform(action, state) {
const lastOf = S._closest(this, state[Stack][0].path.slice(0,state[Stack][0].point-1), MachineNode.type)
if (!lastOf) throw new PathReferenceError(`A relative goto has been provided as a string (${String(action)}), but no state machine exists that this string could be a state of. From path [ ${state[Stack][0].path.slice(0,state[Stack][0].point).map(key => key.toString()).join(', ')} ].`, { instance: this, state, data: { action } })
return { ...state, [Stack]: [ { ...state[Stack][0], path: [...lastOf, action], point: lastOf.length + 1 }, ...state[Stack].slice(1) ] }
}
}, [SequenceGoto]: class SequenceGotoNode extends GotoNode {
static type = SequenceGoto
static typeof(object, objectType, isAction) { return objectType === 'number' }
static perform(action, state) {
const lastOf = S._closest(this, state[Stack][0].path.slice(0,state[Stack][0].point-1), SequenceNode.type)
if (!lastOf) throw new PathReferenceError(`A relative goto has been provided as a number (${String(action)}), but no sequence exists that this number could be an index of from path [ ${state[Stack][0].path.slice(0,state[Stack][0].point).map(key => key.toString()).join(', ')} ].`, { instance: this, state, data: { action } })
return { ...state, [Stack]: [ { ...state[Stack][0], path: [...lastOf, action], point: lastOf.length + 1 }, ...state[Stack].slice(1) ] }
}
}, [ErrorN]: class ErrorNode extends Node {
static type = ErrorN
static typeof = (object, objectType) => (objectType === 'object' && object instanceof Error) || (objectType === 'function' && (object === Error || object.prototype instanceof Error))
static perform(action, state) {
if (typeof action === 'function') throw new action()
throw action
}
}, [Undefined]: class UndefinedNode extends Node {
static type = Undefined
static typeof(object, objectType) { return objectType === 'undefined' }
static execute(node, state) { throw new NodeReferenceError(`There is nothing to execute at path [ ${state[Stack][0].path.slice(0,state[Stack][0].point).map(key => key.toString()).join(', ')} ]`, { instance: this, state, data: { node } }) }
}, [Empty]: class EmptyNode extends Node {
static type = Empty
static typeof(object, objectType) { return object === null }
}, [Continue]: class ContinueNode extends GotoNode {
static type = Continue
static typeof(object, objectType) { return object === Continue }
static perform(action, state) {
const lastOf = S._closest(this, state[Stack][0].path.slice(0,state[Stack][0].point-1), WhileNode.type)
if (!lastOf) throw new PathReferenceError(`A Continue has been used, but no While exists that this Continue could refer to. From path [ ${state[Stack][0].path.slice(0,state[Stack][0].point).map(key => key.toString()).join(', ')} ].`, { instance: this, state, data: { action } })
return { ...state, [Stack]: [ { ...state[Stack][0], path: lastOf, point: lastOf.length }, ...state[Stack].slice(1) ] }
}
}, [Break]: class BreakNode extends GotoNode {
static type = Break
static typeof(object, objectType, isAction) { return object === Break }
static proceed (node, state) {
const lastOf = S._closest(this, state[Stack][0].path.slice(0,state[Stack][0].point-1), WhileNode.type)
if (!lastOf) throw new PathReferenceError(`A Break has been used, but no While exists that this Break could refer to. From path [ ${state[Stack][0].path.slice(0,state[Stack][0].point).map(key => key.toString()).join(', ')} ].`, { instance: this, state, data: { node } })
return S._proceed(this, { ...state, [Stack]: [{ ...state[Stack][0], point: lastOf.length-1 }, ...state[Stack].slice(1)] }, get_path_object(this.process, lastOf.slice(0,-1)))
}
static perform = Node.perform
}, [Return]: class ReturnNode extends GotoNode {
static type = Return
static typeof(object, objectType) { return object === Return || Boolean(object && objectType === 'object' && (Return in object)) }
static perform(action, state) { return { ...state, [Return]: !action || action === Return ? undefined : action[Return], } }
static proceed(action, state) {
if (state[Stack].length === 1) return { ...(state[Stack][0].point === 0 ? { ...state, [Stack]: [] } : S._proceed(this, state, undefined)), [Return]: state[Return] }
const { [Return]: interruptReturn, ...cleanState } = state
return { ...cleanState, [Stack]: state[Stack].slice(1), [state[Stack][0].origin]: interruptReturn }
}
} }
Initialise with an empty process adapters list.
return new S(null).config.adapt // [ ]
Initialise with an empty before
adapters list.
return new S(null).config.before // [ ]
Initialise with an empty after
adapters list.
return new S(null).config.after // [ ]
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' },
Return,
]
}
])
return S._closest(instance, [0, 'then', 1], SequenceNode.type) // [ 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.
const instance = new S()
const result = S._changes(instance, {
[Changes]: {},
preserved: 'value',
common: 'initial',
}, {
common: 'changed',
})
return result // { common: 'changed', preserved: 'value', [Changes]: { common: 'changed' } }
Go through each property in the changes and check they all already exist
Throw a StateReferenceError if a property is referenced that did not previosly exist.
Go through each property and check the JS type is the same as the initial values.
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
Proceed to the next execution path.
const instance = new S([
'firstAction',
'secondAction'
])
return S._proceed(instance, {
[Stack]: [{path:[0],origin:Return,point:1}]
}) // { [Stack]: [ { path: [ 1 ], origin: Symbol(SSSM Return), point: 1 } ] }
Performs fallback logic when a node exits.
const instance = new S([
[
'firstAction',
'secondAction',
],
'thirdAction'
])
return S._proceed(instance, {
[Stack]: [{path:[0,1],origin:Return,point:2}]
}) // { [Stack]: [ { path: [ 1 ], origin: Symbol(SSSM Return), point: 1 } ] }
Gets the type of the given node
let typeofCalled = false
const MyNodeType = Symbol("My Node")
class MyNode extends Node {
static type = MyNodeType
static typeof(object) {
typeofCalled = true
return object === MyNodeType
}
}
S._proceed(
new S(null).addNode(MyNode),
{[Stack]:[{path:[],origin:Return,point:0}]},
MyNodeType,
)
return typeofCalled // true
If the node is unrecognised, throw a TypeEror
return S._proceed(
new S(null),
{[Stack]:[{path:[],origin:Return,point:0}]},
false
) // NodeTypeError
Call the proceed
method of the node to get the next path.
let proceedCalled = false
const MyNodeType = Symbol("My Node")
class MyNode extends Node {
static type = MyNodeType
static typeof(object) { return object === MyNodeType }
static proceed(action, state) {
proceedCalled = true
return { ...state, someChange: 'someValue' }
}
}
const result = S._proceed(
new S(null).addNode(MyNode),
{[Stack]:[{path:[0],origin:Return,point:1}]},
MyNodeType
)
return proceedCalled && result // { someChange: 'someValue' }
Perform actions on the state.
const instance = new S([
'firstAction',
'secondAction',
'thirdAction'
])
return S._perform(instance, { [Stack]: [{path:[0],origin:Return,point:1}], prop: 'value' }, { prop: 'newValue' }) // { prop: 'newValue', [Stack]: [ { path: [ 0 ], origin: Symbol(SSSM Return), point: 1 } ] }
Applies any changes in the given action
to the given state
.
const instance = new S([
'firstAction',
'secondAction',
'thirdAction'
])
return S._perform(instance, { [Stack]: [{path:[0],origin:Return,point:1}], prop: 'value' }, { [Goto]: [2] }) // { prop: 'value', [Stack]: [ { path: [ 2 ], origin: Symbol(SSSM Return), point: 1 } ] }
Gets the node type of the given action
let typeofCalled = false
const MyNodeType = Symbol("My Node")
class MyNode extends Node {
static type = MyNodeType
static typeof(object) {
typeofCalled = true
return object === MyNodeType
}
}
S._perform(
new S(null).addNode(MyNode),
{},
MyNodeType
)
return typeofCalled // true
If the given action
is not recognised, throw a NodeTypeError
return S._perform(
new S(null),
{},
false
) // NodeTypeError
Performs the given action
on the state
let performCalled = false
const MyNodeType = Symbol("My Node")
class MyNode extends Node {
static type = MyNodeType
static typeof(object) { return object === MyNodeType }
static perform(action, state) {
performCalled = true
return { ...state, someChange: 'someValue' }
}
}
const result = S._perform(
new S(null).addNode(MyNode),
{},
MyNodeType
)
return performCalled && result // { someChange: 'someValue' }
S._execute (instance, state = {}, node = get_path_object(instance.process, state[Stack][0].path.slice(0,state[Stack][0].point))))
Executes the node in the process at the state's current path and returns its action.
const instance = new S([
() => ({ result: 'first' }),
() => ({ result: 'second' }),
() => ({ result: 'third' }),
])
return S._execute(instance, { [Stack]: [{path:[1],origin:Return,point: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, { [Stack]: [{path:[1],origin:Return,point:1}] }) // { result: 'second' }
Gets the type of the given node
let typeofCalled = false
const MyNodeType = Symbol("My Node")
class MyNode extends Node {
static type = MyNodeType
static typeof(object) {
typeofCalled = true
return object === MyNodeType
}
}
S._execute(
new S(null).addNode(MyNode),
{},
MyNodeType
)
return typeofCalled // true
If the given node
is not recognised, throw a NodeTypeError
return S._execute(
new S(null),
{},
false
) // NodeTypeError
Execute the given node
and return an action
let executeCalled = false
const MyNodeType = Symbol("My Node")
class MyNode extends Node {
static type = MyNodeType
static typeof(object) { return object === MyNodeType }
static execute() {
executeCalled = true
return 'some action'
}
}
const result = S._execute(
new S(null).addNode(MyNode),
{},
MyNodeType
)
return executeCalled && result // 'some 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
const inputProcess = {
initial: 'swap this',
other: [
{
if: 'swap this too',
then: 'also swap this'
}
]
}
return S._traverse({
process: inputProcess,
config: S.config,
}, (node, path, process, nodeType) => {
if (node === 'swap this') return 'with this'
if (node === 'also swap this') return 'with that'
if (nodeType === ConditionNode.type && node.if === 'swap this too')
return {
...node,
if: 'with another thing'
}
return node
}) // { initial: 'with this', other: [ { if: 'with another thing', then: 'with that' } ] }
Get the node at the given path
Get the type of the node
If the node is not recognised, throw a NodeTypeError
Call the iterator for all nodes as a transformer
Call the primary method
Execute the entire process synchronously.
Will execute the process
const instance = new S({ [Return]: 'return value' })
return S._run(instance) // 'return value'
Will not handle promises even if it is configured
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
.with(asyncPlugin)
return S._run(instance) // undefined
Will not handle promises in sync mode
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
return S._run(instance) // undefined
Is the same as running the executable instance itself
const instance = new S({ [Return]: 'return value' })
return S._run(instance) === instance() // true
Takes the same arguments as the executable instance itself
const instance = new S(({ a, b, c }) => ({ [Return]: `${a} + ${b} - ${c}` }))
.input((a, b, c) => ({ a, b, c }))
return S._run(instance, 1, 2, 3) === instance(1, 2, 3) // true
Extract the useful parts of the config
const instance = new S(null).output(ident).until(() => true)
let gettersAccessed = {}
S._run(
{
config: new Proxy(instance.config, {
get(target, property) {
gettersAccessed[property] = true
return target[property]
}
}),
process: instance.process,
},
{ }
)
return gettersAccessed // { until: true, iterations: true, input: true, output: true, before: true, after: true, defaults: true, trace: true }
Runs the input adapter
let inputAdapterCalled;
S._run(
new S(null).output(ident).until(() => true)
.input((state) => {
inputAdapterCalled = true
return state
}),
{ }
)
return inputAdapterCalled // true
Takes in all arguments passed in after the instance
let passedArgs;
S._run(
new S(null).output(ident).until(() => true)
.input((...args) => {
passedArgs = args
return {}
}),
1, 2, 3, 4
)
return passedArgs // [ 1, 2, 3, 4 ]
the iterations are initialised at 0
let firstValue;
S._run(
new S(null).output(ident)
.until((state, iterations) => {
if (firstValue === undefined)
firstValue = iterations
return state
}),
{ }
)
return firstValue // 0
Before modifiers are called
let beforeAdapterCalled = false
S._run(
new S(null).output(ident).until(() => true)
.before((state) => {
beforeAdapterCalled = true
return state
}),
{ }
)
return beforeAdapterCalled // true
Before adapter are called after input adapter
let inputAdapterCalled = false
let beforeAdapterCalled = false
let beforeAdapterCalledAfterInputAdapter = false
S._run(
new S(null).output(ident).until(() => true)
.input((state) => {
inputAdapterCalled = true
return state
})
.before((state) => {
beforeAdapterCalled = true
if (inputAdapterCalled)
beforeAdapterCalledAfterInputAdapter = true
return state
}),
{ }
)
return inputAdapterCalled && beforeAdapterCalled && beforeAdapterCalledAfterInputAdapter // true
Initial state will be deep merged if enabled
return S._run(
new S(null).output(ident).until(() => true).deep
.defaults({ myProperty: { subProperty: 'otherValue' } }),
{ myProperty: { myOtherProperty: 'myValue' } }
) // { myProperty: { myOtherProperty: 'myValue', subProperty: 'otherValue' } }
Initial state will be merged before passing it into the before modifiers
let initialState = null
S._run(
new S(null).output(ident).until(() => true)
.before((state) => {
initialState = state
return state
}),
{ myProperty: 'myValue' }
)
return initialState // { myProperty: 'myValue', [Stack]: [ { path: [ ], origin: Symbol(SSSM Return), point: 0 } ], [Trace]: [ ], [Changes]: { myProperty: 'myValue' }, [Return]: undefined }
Default to an empty change object
Uses the defaults as an initial state
return S._run(
new S(null).output(ident).until(() => true)
.defaults({ myProperty: 'myValue' }),
{ }
) // { myProperty: 'myValue' }
return S._run(
new S(null).output(ident).until(() => true),
{ [Stack]: [{path:['some','specific','path'],origin:Return,point:3}] }
) // { [Stack]: [ { path: [ 'some', 'specific', 'path' ], origin: Symbol(SSSM Return), point: 3 } ] }
Stack starts as root node path by default.
return S._run(
new S(null).output(ident).until(() => true),
{ }
) // { [Stack]: [ { path: [ ], origin: Symbol(SSSM Return), point: 0 } ] }
Trace can be populated by passing it in
return S._run(
new S([null]).output(ident).until(() => true),
{ [Trace]: [[{path:['some','specific','path'],origin:Return,point:3}]] }
) // { [Trace]: [ [ { path: [ 'some', 'specific', 'path' ], origin: Symbol(SSSM Return), point: 3 } ] ] }
Trace will be an empty list by default.
return S._run(
new S(null).output(ident).until(() => true),
{ }
) // { [Trace]: [ ] }
return S._run(new S(null).output(ident), { [Return]: 'myValue' }) // { [Return]: 'myValue' }
Do not define a return value by default
return S._run(new S(null).output(ident), { }) // { [Return]: undefined }
Changes will be empty after initialisation
return S._run(
new S(null).output(ident).before(function (state) {
return this.changes(state, { myOtherProperty: 'myOtherValue' })
}),
{ myProperty: 'myValue' }
) // { myProperty: 'myValue', myOtherProperty: 'myOtherValue', [Changes]: { myProperty: undefined, myOtherProperty: undefined } }
Changes can be populated by passing it in
return S._run(
new S(null).output(ident).before(function (state) {
return this.changes(state, { myOtherProperty: 'myOtherValue' })
}),
{ myProperty: 'myValue', [Changes]: { myProperty: 'anything' } }
) // { myProperty: 'myValue', myOtherProperty: 'myOtherValue', [Changes]: { myProperty: 'anything', myOtherProperty: undefined } }
This should be fine for most finite machines, but may be too little for some constantly running machines.
let untilCalled = false
S._run(new S(
() => ({ myProperty: 'myValue' })
).until(() => {
untilCalled = true
return true
}).output(ident), {
[Stack]: [{path:[],origin:Return,point:0}],
})
return untilCalled // true
Do it first to catch starting with a Return
in place.
return S._run(new S(
() => ({ myProperty: 'myValue' })
).output(ident), {
[Stack]: [{path:[],origin:Return,point:0}],
[Return]: 'myValue'
}) // { myProperty: undefined, [Stack]: [ { path: [ ], origin: Symbol(SSSM Return), point: 0 } ], [Return]: 'myValue' }
If the iterations are exceeded, Error
return S._run(new S([
() => ({ myProperty: 'myValue' })
]).for(1).trace.output(ident), {
[Stack]: [{path:[],origin:Return,point:0}],
[Trace]: [],
}) // MaxIterationsError
If stack trace is enabled, push the current path to the stack
return S._run(new S(
() => ({ myProperty: 'myValue' })
).until((_,runs)=>runs>=1).trace.output(ident), {
[Stack]: [{path:[],origin:Return,point:0}],
[Trace]: [],
}) // { [Trace]: [ [ { path: [ ], origin: Symbol(SSSM Return), point: 0 } ] ] }
Executes the current node on the process, returning the action to perform
return S._run(new S(
() => ({ myProperty: 'myValue' })
).until((_,runs)=>runs>=1).output(ident), {
[Stack]: [{path:[],origin:Return,point:0}]
}) // { myProperty: 'myValue' }
Performs any required actions. Updating the currentState
return S._run(new S([
{ myProperty: 'myValue' }
]).until((_,runs)=>runs>=1).output(ident), {
[Stack]: [{path:[0],origin:Return,point:1}]
}) // { myProperty: 'myValue' }
Proceeds to the next action
return S._run(new S([
null,
null
]).until((_,runs)=>runs>=1).output(ident), {
[Stack]: [{path:[0],origin:Return,point:1}]
}) // { [Stack]: [ { path: [ 1 ], origin: Symbol(SSSM Return), point: 1 } ] }
When returning, run the end state adapters, then the output adapter to complete execution.
let adaptOutputCalled = false
let afterCalled = false
let adaptOutputCalledAfterAfter = false
new S(Return)
.after((state) => {
afterCalled = true
return state
})
.output((state) => {
adaptOutputCalled = true
if (afterCalled)
adaptOutputCalledAfterAfter = true
return state
})
({
myProperty: 'myValue'
})
return adaptOutputCalled && afterCalled && adaptOutputCalledAfterAfter // true
Throws the given error
The ErrorN symbol is exported as { ErrorN }
import { ErrorN } from './index.js'
return ErrorN; // success
This definition is exported by the library as { ErrorNode }
import { ErrorNode } from './index.js'
return ErrorNode; // success
Uses the ErrorN symbol as the type.
return ErrorNode.type // Symbol(SSSM Error)
Matches error objects
return S.config.nodes.typeof(new Error('My Error')) // Symbol(SSSM Error)
Matches error constructors
return S.config.nodes.typeof(Error) // Symbol(SSSM Error)
Matches descendent error objects
return S.config.nodes.typeof(new TypeError('My Error')) // Symbol(SSSM Error)
Matches descendent error constructors
return S.config.nodes.typeof(TypeError) // Symbol(SSSM Error)
Throw an error constructed by the function.
return ErrorNode.perform(TestError, {}) // TestError
Throw an existing error instance.
return ErrorNode.perform(new TestError(), {}) // TestError
Updates the state by merging the properties. Arrays will not be merged.
Overrides existing properties when provided
const instance = new S({ result: 'overridden' })
.output(({ result }) => result)
return instance({ result: 'start' }) // 'overridden'
Adds new properties while preserving existing properties
const instance = new S({ newValue: true })
.output(state => state)
return instance({ existingValue: true }) // { existingValue: true, newValue: true }
The Changes symbol is exported as { Changes }
import { Changes } from './index.js'
return Changes; // success
This definition is exported by the library as { ChangesNode }
import { ChangesNode } from './index.js'
return ChangesNode; // success
Uses the Changes symbol as the type.
return ChangesNode.type // Symbol(SSSM Changes)
Any object not caught by other conditions should qualify as a state change.
return S.config.nodes.typeof({ someProperty: 'someValue' }) // Symbol(SSSM Changes)
Apply the changes to the state and step forward to the next node
return ChangesNode.perform.call(new S(), { myProperty: 'changed' }, { [Changes]: {}, myProperty: 'myValue' }) // { myProperty: 'changed', [Changes]: { myProperty: 'changed' } }
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' }),
]).output(({ result }) => result)
return instance({ result: 'start' }) // 'start addition1 addition2'
The Sequence symbol is exported as { Sequence }
import { Sequence } from './index.js'
return Sequence; // success
This definition is exported by the library as { SequenceNode }
import { SequenceNode } from './index.js'
return SequenceNode; // success
Uses the Sequence symbol as the type.
return SequenceNode.type // Symbol(SSSM Sequence)
Get the current index in this sequence from the path
return SequenceNode.proceed.call(new S([[null,null,null], null]), [null,null,null], { [Stack]: [{path:[0,1],origin:Return,point:1}]}) // { [Stack]: [ { path: [ 0, 2 ], origin: Symbol(SSSM Return), point: 2 } ] }
Execute the next node
Proceed as normal if the list is complete
return SequenceNode.proceed.call(new S([[null,null,null], null]), [null,null,null], { [Stack]: [{path:[0,2],origin:Return,point:1}]}) // { [Stack]: [ { path: [ 1 ], origin: Symbol(SSSM Return), point: 1 } ] }
A sequence is an array.
return S.config.nodes.typeof([ 1, 2, 3 ]) // Symbol(SSSM Sequence)
return S.config.nodes.typeof([ 1, 2, 3 ], 'object', true) // Symbol(SSSM Absolute Goto)
Execute a sequence by directing to the first node (so long as it has nodes)
return SequenceNode.execute([null,null,null], { [Stack]: [{path:['some',0,'complex','path'],origin:Return,point:4}]}) // [ 'some', 0, 'complex', 'path', 0 ]
Traverse a sequence by iterating through each node 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' }))
.output(({ result }) => result)
return instance({ result: 'start' }) // 'start addition'
A function can return a goto
const instance = new S([
{ result: 'first' },
() => 4,
{ result: 'skipped' },
Return,
{ result: 'second' },
]).output(({ result }) => result)
return instance({ result: 'start' }) // 'second'
A function can return a return statement
const instance = new S(() => ({ [Return]: 'changed' }))
return instance() // 'changed'
A function can do anything without needing to return (set and forget)
const instance = new S(() => {
// Arbitrary code
}).output(({ result }) => result)
return instance({ result: 'start' }) // 'start'
The FunctionN symbol is exported as { FunctionN }
import { FunctionN } from './index.js'
return FunctionN; // success
This definition is exported by the library as { FunctionNode }
import { FunctionNode } from './index.js'
return FunctionNode; // success
Uses the FunctionN symbol as the type.
return FunctionNode.type // Symbol(SSSM Function)
A function is a JS function. A function cannot be an action.
return S.config.nodes.typeof(() => {}) // Symbol(SSSM Function)
Exectute a functon by running it, passing in the state.
let methodRun = false
const result = FunctionNode.execute((...args) => {
methodRun = true
return args
}, { [Stack]: [{path:[],origin:Return,point:0}] })
return methodRun && result // [ { [Stack]: [ { path: [ ], origin: Symbol(SSSM Return), point: 0 } ] } ]
The Undefined symbol is exported as { Undefined }
import { Undefined } from './index.js'
return Undefined; // success
This definition is exported by the library as { UndefinedNode }
import { UndefinedNode } from './index.js'
return UndefinedNode; // success
Uses the Undefined symbol as the type.
return UndefinedNode.type // Symbol(SSSM Undefined)
Undefined is the undefined
keyword.
return S.config.nodes.typeof(undefined) // Symbol(SSSM Undefined)
Un undefined node cannot be executed, throw an error to help catch incorrect configuration.
const instance = new S([undefined])
return instance() // NodeReferenceError
When used as an action, undefined only moves to the next node.
const instance = new S([
() => undefined,
{ [Return]: 'second' }
])
return instance() // 'second'
The Empty symbol is exported as { Empty }
import { Empty } from './index.js'
return Empty; // success
This definition is exported by the library as { EmptyNode }
import { EmptyNode } from './index.js'
return EmptyNode; // success
Uses the Empty symbol as the type.
return EmptyNode.type // Symbol(SSSM Empty)
Empty is the null
keyword.
return S.config.nodes.typeof(null) // Symbol(SSSM Empty)
Empty is a no-op, and will do nothing except move to the next node
const instance = new S([null, { result: 'second' }, () => null])
.output(({ result }) => result)
return instance({ result: 'start' }) // 'second'
The Condition symbol is exported as { Condition }
import { Condition } from './index.js'
return Condition; // success
This definition is exported by the library as { ConditionNode }
import { ConditionNode } from './index.js'
return ConditionNode; // success
Uses the Condition symbol as the type.
return ConditionNode.type // Symbol(SSSM Condition)
A condition is an object with the 'if'
property. A condition cannot be an action.
return S.config.nodes.typeof({ if: false }) // Symbol(SSSM Condition)
Defines 'if', 'then', 'else'
keywords
return ConditionNode.keywords // [ 'if', 'then', 'else' ]
Evaluate the 'if'
property as a function that depends on the state.
let switchMethodRun = false
ConditionNode.execute({ if: () => {
switchMethodRun = true
return 'someKey'
}, then: null }, { [Stack]: [{path:[],origin:Return,point:0}] })
return switchMethodRun // true
If truthy, direct to the 'then'
clause if it exists
const instance = new S({
if: ({ input }) => input === 'the same',
then: { [Return]: 'truthy' },
else: { [Return]: 'falsey' },
})
return instance({ input: 'the same' }) // 'truthy'
Otherwise, direct to the 'else'
clause if it exists
const instance = new S({
if: ({ input }) => input === 'the same',
then: { [Return]: 'truthy' },
else: { [Return]: 'falsey' },
})
return instance({ input: 'NOT the same' }) // 'falsey'
Copy over the original properties to preserve any custom symbols.
Iterate on the 'then'
clause if it exists
Iterate on the 'else'
clause if it exists
Iterate over any symbols specified
const instance = new S({
switch: ({ input }) => input,
case: {
start: { [Return]: 'first' },
two: { [Return]: 'second' },
default: { [Return]: 'none' },
}
})
const output1 = instance({ input: 'start' })
const output2 = instance({ input: 'two' })
const output3 = instance({ input: 'other' })
return { output1, output2, output3 } // { output1: 'first', output2: 'second', output3: 'none' }
The Switch symbol is exported as { Switch }
import { Switch } from './index.js'
return Switch; // success
This definition is exported by the library as { SwitchNode }
import { SwitchNode } from './index.js'
return SwitchNode; // success
Uses the Switch symbol as the type.
return SwitchNode.type // Symbol(SSSM Switch)
A switch node is an object with the 'switch'
property.
return S.config.nodes.typeof({ switch: false }) // Symbol(SSSM Switch)
Defines 'switch', 'case', 'default'
keywords.
return SwitchNode.keywords // [ 'switch', 'case', 'default' ]
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.
let switchMethodRun = false
SwitchNode.execute({ switch: () => {
switchMethodRun = true
return 'someKey'
}, case: { someKey: null } }, { [Stack]: [{path:[],origin:Return,point:0}] })
return switchMethodRun // true
If the key exists in the 'case'
caluses, use the key, otherwise use the 'default'
clause
return SwitchNode.execute({ switch: 'extant', case: { extant: null } }, { [Stack]: [{path:[],origin:Return,point:0}] }) // [ 'case', 'extant' ]
return SwitchNode.execute({ switch: 'non extant', case: { default: null } }, { [Stack]: [{path:[],origin:Return,point:0}] }) // [ 'case', 'default' ]
Check again if the key exists ('default'
clause may not be defined), if it does, redirect to the case, otherwise do nothing.
return SwitchNode.execute({ switch: 'non extant', case: {} }, { [Stack]: [{path:[],origin:Return,point:0}] }) // null
Traverse a switch by iterating over the 'case'
clauses
Repeatedly executes the 'do' clause, so long as the 'while' condition is true
The While symbol is exported as { While }
import { While } from './index.js'
return While; // success
This definition is exported by the library as { WhileNode }
import { WhileNode } from './index.js'
return WhileNode; // success
Uses the While symbol as the type.
return WhileNode.type // Symbol(SSSM While)
A while node is an object with the 'while'
property.
return S.config.nodes.typeof({ while: false }) // Symbol(SSSM While)
Defines 'while
, 'do'` keywords
return WhileNode.keywords // [ 'while', 'do' ]
return WhileNode.execute({ while: false, do: null }, { [Stack]: [{path:[0,0],origin:Return,point:2}] }) // null
If the condition is false, exit the while loop.
If true
, execute the 'do'
clause
return WhileNode.execute({ while: true, do: null }, { [Stack]: [{path:[0,0],origin:Return,point:2}] }) // [ 0, 0, 'do' ]
Proceed by re-entering the while loop.
return WhileNode.proceed(undefined, { [Stack]: [{path:['some',0,'complex',0,'path'],origin:Return,point:2}] }) // { [Stack]: [ { path: [ 'some', 0 ], origin: Symbol(SSSM Return), point: 2 } ] }
Traverse a while by iterating over the 'do'
clause
const instance = new S({
initial: [
() => ({ result: 'first' }),
'next',
],
next: { result: 'second' }
}).output(({ result }) => result)
return instance({ result: 'start' }) // 'second'
The Machine symbol is exported as { Machine }
import { Machine } from './index.js'
return Machine; // success
This definition is exported by the library as { MachineNode }
import { MachineNode } from './index.js'
return MachineNode; // success
Uses the Machine symbol as the type.
return MachineNode.type // Symbol(SSSM Machine)
A machine is an object with the 'initial'
property. A machine cannot be used as an action.
return S.config.nodes.typeof({ initial: null }) // Symbol(SSSM Machine)
Defines 'initial'
keyword.
return MachineNode.keywords // [ 'initial' ]
Execute a machine by directing to the 'initial'
stages.
return MachineNode.execute(undefined, { [Stack]: [{path:['a','b','c'],oirign:Return,point:3}]}) // [ 'a', 'b', 'c', 'initial' ]
Traverse a machine by iterating over all the stages
Transitioning is also possible by using and object with the Goto
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' },
{ [Goto]: 'next' }
],
next: { result: 'second' }
}).output(({ result }) => result)
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' },
{ [Goto]: 'next', result: 'ignored' }
],
next: Return
}).output(({ result }) => result)
return instance({ result: 'start' }) // 'first'
The Goto symbol is exported as { Goto }
import { Goto } from './index.js'
return Goto; // success
This definition is exported by the library as { GotoNode }
import { GotoNode } from './index.js'
return GotoNode; // success
Uses the Goto symbol as the type.
return GotoNode.type // Symbol(SSSM Goto)
A goto is an object with the Goto
property.
return S.config.nodes.typeof({ [Goto]: 'stage' }) // Symbol(SSSM Goto)
A goto is performed by performing the value of the Goto
property to allow for using absolute or relative gotos
let performCalled = false
const MyNodeType = Symbol("My Node")
class MyNode extends Node {
static type = MyNodeType
static typeof(object) { return object === MyNodeType }
static perform(action, state) {
performCalled = true
return { ...state, someChange: 'someValue' }
}
}
const result = GotoNode.perform.call(new S().addNode(MyNode), { [Goto]: MyNodeType }, {})
return performCalled && result // { someChange: 'someValue' }
A goto does not require proceeding, simply return the current state unmodified
const stateObj = { myProperty: 'myValue' }
return GotoNode.proceed(undefined, stateObj) === stateObj // true
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([
2,
{ [Return]: 'skipped' },
{ [Return]: 'second' },
])
return instance() // '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: ({ input }) => input === 'skip',
then: 2,
else: 1
},
{ [Return]: 'skipped' },
{ [Return]: 'second' },
])
return instance({ input: 'skip' }) // 'second'
The SequenceGoto symbol is exported as { SequenceGoto }
import { SequenceGoto } from './index.js'
return SequenceGoto; // success
This definition is exported by the library as { SequenceGotoNode }
import { SequenceGotoNode } from './index.js'
return SequenceGotoNode; // success
Uses the SequenceGoto symbol as the type.
return SequenceGotoNode.type // Symbol(SSSM Sequence Goto)
A sequence goto is a number.
return S.config.nodes.typeof(8) // Symbol(SSSM Sequence Goto)
Get the closest ancestor that is a sequence.
return SequenceGotoNode.perform.call(new S([[null,{initial:null}]]), 2, { [Stack]: [{path:[0,1,'initial'],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 0, 2 ], origin: Symbol(SSSM Return), point: 2 } ] }
If there is no such ancestor, throw a PathReferenceError
return SequenceGotoNode.perform.call(new S({initial:{initial:{initial:null}}}), 'myStage', { [Stack]: [{path:['initial','initial','initial'],origin:Return,point:3}] }) // PathReferenceError
Update the path to the parent > index
return SequenceGotoNode.perform.call(new S([[null,null]]), 2, { [Stack]: [{path:[0,0],origin:Return,point:2}] }) // { [Stack]: [ { path: [ 0, 2 ], origin: Symbol(SSSM Return), point: 2 } ] }
Gotos 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' }
}).output(({ result }) => result)
return instance({ result: 'start' }) // 'second'
The MachineGoto symbol is exported as { MachineGoto }
import { MachineGoto } from './index.js'
return MachineGoto; // success
This definition is exported by the library as { MachineGotoNode }
import { MachineGotoNode } from './index.js'
return MachineGotoNode; // success
Uses the MachineGoto symbol as the type.
return MachineGotoNode.type // Symbol(SSSM Machine Goto)
A machine goto is a string.
return S.config.nodes.typeof('stage') // Symbol(SSSM Machine Goto)
Get the closest ancestor that is a machine.
return MachineGotoNode.perform.call(new S({initial:[{ initial: [null] }]}), 'myStage', { [Stack]: [{path:['initial',0,'initial',0],origin:Return,point:4}] }) // { [Stack]: [ { path: [ 'initial', 0, 'myStage' ], origin: Symbol(SSSM Return), point: 3 } ] }
If no machine ancestor is found, throw a PathReferenceError
return MachineGotoNode.perform.call(new S([null]), 'myStage', { [Stack]: [{path:[0],origin:Return,point:1}] }) // PathReferenceError
Update the path to parent > stage
return MachineGotoNode.perform.call(new S([[{ initial: null }]]), 'myStage', { [Stack]: [{path:[0,0,'initial'],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 0, 0, 'myStage' ], origin: Symbol(SSSM Return), point: 3 } ] }
Interrupts are like gotos, except they will return to the previous execution path once complete.
Interrupts a way of performing other paths, then returning to the current path, using the symbol of a neghboring interrupt you can direct flow through a state machine.
const interrupt = Symbol('interrupt')
const instance = new S({
initial: [
{ result: 'first' },
interrupt
],
[interrupt]: { result: 'second' }
}).output(({ result }) => result)
return instance({ result: 'start' }) // 'second'
The InterruptGoto symbol is exported as { InterruptGoto }
import { InterruptGoto } from './index.js'
return InterruptGoto; // success
This definition is exported by the library as { InterruptGotoNode }
import { InterruptGotoNode } from './index.js'
return InterruptGotoNode; // success
Uses the InterruptGoto symbol as the type.
return InterruptGotoNode.type // Symbol(SSSM Interrupt Goto)
An interrupt goto is a symbol.
return S.config.nodes.typeof(testSymbol) // Symbol(SSSM Interrupt Goto)
Get the closest ancestor that contains this interrupt symbol.
return InterruptGotoNode.perform.call(new S({ [testSymbol]: null, initial:{ [testSymbol]: null,initial: { initial: null } } }), testSymbol, { [Stack]: [{path:['initial','initial','initial'],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 'initial', Symbol(test symbol) ], origin: Symbol(test symbol), point: 2 }, { path: [ 'initial', 'initial', 'initial' ], origin: Symbol(SSSM Return), point: 3 } ] }
If no suitable ancestor is found, return the interrupt symbol itself.
return InterruptGotoNode.perform.call(new S([[{ initial: null }]]), testSymbol, { [Stack]: [{path:[0,0,'initial'],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 0, 0, 'initial' ], origin: Symbol(SSSM Return), point: 3 } ], [Return]: Symbol(test symbol) }
Update the path to parent > interrupt
return InterruptGotoNode.perform.call(new S([[{ [testSymbol]: null, initial: null }]]), testSymbol, { [Stack]: [{path:[0,0,'initial'],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 0, 0, Symbol(test symbol) ], origin: Symbol(test symbol), point: 3 }, { path: [ 0, 0, 'initial' ], origin: Symbol(SSSM Return), point: 3 } ] }
An interrupt goto proceeds the path previous to it, but preserves the interrupts place at the top of the stack.
If the interrupt is trying to return, proceed as if returning.
Proceed the stack before this point, and strip out the affected system properties.
return InterruptGotoNode.proceed.call(new S([ null, null ]), testSymbol, { [Stack]: [{path:['first','item'],origin:testSymbol,point:2},{path:[0],origin:Return,point:1}] }) // { [Stack]: [ { path: [ 'first', 'item' ], origin: Symbol(test symbol), point: 2 }, { path: [ 1 ], origin: Symbol(SSSM Return), point: 1 } ] }
Add the current interrupt back in to the resulting stack.
return InterruptGotoNode.proceed.call(new S(), testSymbol, { [Stack]: [{path:['first','item'],origin:testSymbol,point:2},{path:[],origin:Return,point:0}] }) // { [Stack]: [ { path: [ 'first', 'item' ], origin: Symbol(test symbol), point: 2 } ] }
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 Goto
symbol as a key, with the array as the value, or returned by an action.
Using an absolute goto in a goto object works
const instance = new S({
initial: [
{ result: 'first' },
{ [Goto]: ['next',1] }
],
next: [
{ result: 'skipped' },
({ result }) => ({ [Return]: result }),
]
})
return instance({ result: 'start' }) // 'first'
Using an absolute goto as a return value works
const instance = new S({
initial: [
{ result: 'first' },
() => ['next',1]
],
next: [
{ result: 'skipped' },
({ result }) => ({ [Return]: result }),
]
})
return instance({ result: 'start' }) // 'first'
Using an absolute goto as an action does NOT work.
const instance = new S({
initial: [
{ result: 'first' },
['next',1]
],
next: [
{ result: 'not skipped' },
({ result }) => ({ [Return]: result }),
]
})
return instance({ result: 'start' }) // 'not skipped'
The AbsoluteGoto symbol is exported as { AbsoluteGoto }
import { AbsoluteGoto } from './index.js'
return AbsoluteGoto; // success
This definition is exported by the library as { AbsoluteGotoNode }
import { AbsoluteGotoNode } from './index.js'
return AbsoluteGotoNode; // success
Uses the AbsoluteGoto symbol as the type.
return AbsoluteGotoNode.type // Symbol(SSSM Absolute Goto)
An absolute goto 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.
const path = [1,'stage',testSymbol]
return S.config.nodes.typeof(path, typeof path, true) // Symbol(SSSM Absolute Goto)
An absolute goto is performed by setting Stack
to the path
return AbsoluteGotoNode.perform(['a','b','c'], { [Stack]: [{path:[],origin:Return,point:0}] }) // { [Stack]: [ { path: [ 'a', 'b', 'c' ], origin: Symbol(SSSM Return), point: 3 } ] }
Causes the entire process to terminate immediately and return, setting Return
to true
on the state.
If the symbol is used with a .output
configuration, then it will return according to the given method.
const instance = new S(Return)
.output(({ result }) => result)
return instance({ result: 'start' }) // 'start'
If the symbol is used on its own, then it will simply return undefined
.
const instance = new S(Return)
return instance({ result: 'start' }) // undefined
Using the return symbol as the key to an object will set the return property to that value before returning.
const instance = new S({ [Return]: 'custom' })
return instance() // 'custom'
const instance = new S({ [Return]: 'custom' })
return instance.output(state => state)({ result: 'start' }) // { result: 'start', [Return]: 'custom' }
The Return symbol is exported as { Return }
import { Return } from './index.js'
return Return; // success
This definition is exported by the library as { ReturnNode }
import { ReturnNode } from './index.js'
return ReturnNode; // success
Uses the Return symbol as the type.
return ReturnNode.type // Symbol(SSSM Return)
A return node is the Return
symbol itself, or an object with an Return
property.
return S.config.nodes.typeof(Return) // Symbol(SSSM Return)
return S.config.nodes.typeof({ [Return]: 'value' }) // Symbol(SSSM Return)
Perform a return by setting the Return
property on the state to the return value
return ReturnNode.perform({ [Return]: 'myValue' }, {}) // { [Return]: 'myValue' }
If there are no more paths left, return with the existing value.
return ReturnNode.proceed.call(new S(null), undefined, {
[Stack]: [{path:[],origin:Return,point:0}],
[Return]: 'myValue'
}) // { [Stack]: [ ], [Return]: 'myValue' }
If there are paths left, intercept the return value
return ReturnNode.proceed.call(new S({initial:null,[testSymbol]:null}), undefined, {
[Stack]: [{path:[testSymbol],origin:testSymbol,point:1},{path:[],origin:Return,point:0}],
[Return]: 'myValue'
}) // { [Return]: undefined }
Set the interrupt which was the origin of the thread to the return value in the state.
return ReturnNode.proceed.call(new S({initial:null,[testSymbol]:null}), undefined, {
[Stack]: [{path:[],origin:testSymbol,point:1},{path:[],origin:Return,point:0}],
[Return]: 'myValue'
}) // { [testSymbol]: 'myValue' }
Exit this pass of a While loop and evaluate the condition again.
The Continue symbol is exported as { Continue }
import { Continue } from './index.js'
return Continue; // success
This definition is exported by the library as { ContinueNode }
import { ContinueNode } from './index.js'
return ContinueNode; // success
Uses the Continue symbol as the type.
return ContinueNode.type // Symbol(SSSM Continue)
Looks for the Continue symbol specifically.
return S.config.nodes.typeof(Continue) // Symbol(SSSM Continue)
Find the closest While loop.
return ContinueNode.perform.call(new S([{ while: true, do: {while:true,do:null} }, null]), Continue, { [Stack]: [{path:[0,'do','do'],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 0, 'do' ], origin: Symbol(SSSM Return), point: 2 } ] }
If there is none, throw a PathReferenceError
.
return ContinueNode.perform.call(new S(null), Continue, { [Stack]: [{path:[],origin:Return,point:0}] }) // PathReferenceError
Modify the stack to point to the closest While loop.
return ContinueNode.perform.call(new S([{ while: true, do: [null] }, null]), Continue, { [Stack]: [{path:[0,'do',0],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 0 ], origin: Symbol(SSSM Return), point: 1 } ] }
Break out of a while loop, and proceed as if the condition has failed.
The Break symbol is exported as { Break }
import { Break } from './index.js'
return Break; // success
This definition is exported by the library as { BreakNode }
import { BreakNode } from './index.js'
return BreakNode; // success
Uses the Break symbol as the type.
return BreakNode.type // Symbol(SSSM Break)
Looks for the Break symbol specifically.
return S.config.nodes.typeof(Break) // Symbol(SSSM Break)
Find the closest While loop.
return BreakNode.proceed.call(new S([{ while: true, do: {while:true,do:null} }, null]), Break, { [Stack]: [{path:[0,'do','do'],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 0 ], origin: Symbol(SSSM Return), point: 1 } ] }
If there is none, throw a PathReferenceError
.
return BreakNode.proceed.call(new S(null), Break, { [Stack]: [{path:[],origin:Return,point:0}] }) // PathReferenceError
Proceed on the While loop as if it is exiting.
return BreakNode.proceed.call(new S([{ while: true, do: [null] }, null]), Break, { [Stack]: [{path:[0,'do',0],origin:Return,point:3}] }) // { [Stack]: [ { path: [ 1 ], origin: Symbol(SSSM Return), point: 1 } ] }
Perform by doing nothing, do not inherit from GotoNode
.
const stateObj = { myProperty: 'myValue' }
return BreakNode.perform(Break, stateObj) === stateObj // true
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
Is an error instance
return (new SuperSmallStateMachineError()) instanceof Error // true
A message string can be passed into the error
return new SuperSmallStateMachineError('My String!') // { message: 'My String!' }
All exported errors inherit from this class
const referenceError = new SuperSmallStateMachineReferenceError()
const typeError = new SuperSmallStateMachineTypeError()
const stateReferenceError = new StateReferenceError()
const stateTypeError = new StateTypeError()
const nodeTypeError = new NodeTypeError()
const nodeReferenceError = new NodeReferenceError()
const maxIterationsError = new MaxIterationsError()
const pathReferenceError = new PathReferenceError()
return referenceError instanceof SuperSmallStateMachineError
&& typeError instanceof SuperSmallStateMachineError
&& stateReferenceError instanceof SuperSmallStateMachineError
&& stateTypeError instanceof SuperSmallStateMachineError
&& nodeTypeError instanceof SuperSmallStateMachineError
&& nodeReferenceError instanceof SuperSmallStateMachineError
&& maxIterationsError instanceof SuperSmallStateMachineError
&& pathReferenceError instanceof SuperSmallStateMachineError // true
Passing a state, instance, and/or data will make those properties available in the error
return new SuperSmallStateMachineError('', {
instance: 'something',
state: 'my state',
data: 'special data'
}) // { instance: 'something', state: 'my state', data: 'special data' }
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 exported reference errors inherit from this class
const stateReferenceError = new StateReferenceError()
const nodeReferenceError = new NodeReferenceError()
const pathReferenceError = new PathReferenceError()
return stateReferenceError instanceof SuperSmallStateMachineReferenceError
&& nodeReferenceError instanceof SuperSmallStateMachineReferenceError
&& pathReferenceError instanceof SuperSmallStateMachineReferenceError // true
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
All exported type errors inherit from this class
const stateTypeError = new StateTypeError()
const nodeTypeError = new NodeTypeError()
return stateTypeError instanceof SuperSmallStateMachineTypeError
&& nodeTypeError instanceof SuperSmallStateMachineTypeError // true
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 reference error is thrown when a new property is added to the state of a machine while in strict mode
const machine = new S({ myUnknownVar: true }).strict
return machine() // StateReferenceError
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 state type errors is thrown when a property changes type in strict state mode
const machine = new S({ myKnownVar: 'not a boolean' }).strictTypes
.defaults({ myKnownVar: true })
return machine() // StateTypeError
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
A node type error is thrown when an unrecognised node is used in a process
const machine = new S(true)
return machine() // NodeTypeError
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 { NodeReferenceError }
import { NodeReferenceError } from './index.js'
return NodeReferenceError; // success
An node reference error is thrown when a node in a process is undefined
const machine = new S([undefined])
return machine() // NodeReferenceError
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 max iterations errors is thrown when an execution exceeds the maximum allowed iterations
const machine = new S([ 0 ]).for(10)
return machine() // MaxIterationsError
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
A path reference error is thrown when the machine is told to target a node that does not exist
const machine = new S('not a stage')
return machine() // PathReferenceError