@oversword/super-small-state-machine
v5.2.1
Published
A tiny, stand-alone state machine executor that uses an extremely simple syntax
Downloads
2
Maintainers
Readme
Language
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
Tutorial
To create a new state machine, create a new instance of the S class
const instance = new S() // SucceedsThe 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
}) // SucceedsTo configure the state machine, you can cahin configuration methods
const instance = new S()
.deep
.strict
.forever // SucceedsYou 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) // SucceedsInstance
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 }Instance Constructor
Basics
The primary way of interacting with this library is to create a new instance
const instance = new S() // SucceedsInstances are executable, like functions
const instance = new S()
return instance() === undefined // SucceedsThe constructor takes two arguments, the process and the config
const instance = new S({}, {})
return instance() // SucceedsNeither 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() // SucceedsThe executable instance will be an instanceof ExecutableFunction
It will execute the run or override method in scope of the new SuperSmallStateMachine instance.
Create the config by merging the passed config with the defaults.
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 // trueconst 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 // trueinstance.closest (path = [], ...nodeTypes)
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' ]instance.changes (state = {}, changes = {})
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' } }instance.proceed (state = {}, node = undefined)
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 } ] }instance.perform (state = {}, action = null)
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' }instance.execute (state = {}, node = undefined)
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' }instance.traverse(iterator = a => a)
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' } ] }instance.run (...input)
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() // undefinedWill not handle promises in sync mode
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
return instance.run() // undefinedIs the same as running the executable instance itself
const instance = new S({ [Return]: 'return value' })
return instance.run() === instance() // trueTakes 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) // trueinstance.do(process) <default: null>
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'instance.defaults(defaults) <default: {}>
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'instance.input(input) <default: (state => state)>
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'instance.output(output) <default: (state => state.output)>
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'instance.untrace
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() // [ ]instance.trace
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 } ] ]instance.shallow
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 } } }instance.deep
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 } } }instance.unstrict
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() // StateReferenceErrorconst instance = new S(() => ({ unknownVariable: false}))
.defaults({ knownVariable: true })
.strict
.unstrict
return instance() // Succeedsinstance.strict
Checks state properties when an state change is made.
Creates a new instance.
const instance = new S(() => ({ unknownVariable: false}))
.defaults({ knownVariable: true })
return instance() // Succeedsconst instance = new S(() => ({ unknownVariable: false}))
.defaults({ knownVariable: true })
.strict
return instance() // StateReferenceErrorinstance.strictTypes
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() // StateTypeErrorinstance.for(iterations = 10000) <default: 10000>
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() // MaxIterationsErrorinstance.until(until) <default: (state => Return in state)>
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'instance.forever
Removes the max iteration limit.
Creates a new instance.
const instance = new S().forever
return instance.config.iterations // Infinityinstance.override(override) <default: instance.run>
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'instance.addNode(...nodes)
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'instance.adapt(...adapters)
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'instance.before(...adapters)
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'instance.after(...adapters)
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'instance.with(...adapters)
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; // successThe main class is exported as { SuperSmallStateMachine }
import { SuperSmallStateMachine } from './index.js'
return SuperSmallStateMachine; // successThe node class is exported as { NodeDefinition }
import { NodeDefinition } from './index.js'
return NodeDefinition; // successThe node collection class is exported as { NodeDefinitions }
import { NodeDefinitions } from './index.js'
return NodeDefinitions; // successThe node class is exported as { Node }
import { Node } from './index.js'
return Node; // successThe node collection class is exported as { Nodes }
import { Nodes } from './index.js'
return Nodes; // successChain
S.closest (path = [], ...nodeTypes)
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' ]S.changes (state = {}, changes = {})
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' } }S.proceed (state = {}, action = undefined)
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 } ] }S.perform (state = {}, action = null)
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' }S.execute (state = {}, node = undefined)
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' }S.traverse(iterator = a => a)
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' } ] }S.run (...input)
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) // undefinedWill not handle promises in sync mode
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
return S.run()(instance) // undefinedIs the same as running the executable instance itself
const instance = new S({ [Return]: 'return value' })
return S.run()(instance) === instance() // trueTakes 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) // trueS.do(process) <default: null>
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'S.defaults(defaults) <default: {}>
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'S.input(input) <default: (state => state)>
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'S.output(output) <default: (state => state[Return])>
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'S.untrace
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() // [ ]S.trace
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 } ] ]S.shallow
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 } } }S.deep
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 } } }S.unstrict
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() // StateReferenceErrorThe 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() // SucceedsS.strict
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() // SucceedsWith 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() // StateReferenceErrorS.strictTypes
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() // StateTypeErrorS.for(iterations = 10000) <default: 10000>
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() // MaxIterationsErrorS.until(until) <default: (state => Return in state)>
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'S.forever
Removes the max iteration limit.
Will modify the given instance.
const instance = new S().with(S.forever)
return instance.config.iterations // InfinityS.override(override) <default: instance.run>
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'S.addNode(...nodes)
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'S.adapt(...adapters)
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'S.before(...adapters)
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'S.after(...adapters)
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'S.with(...adapters)
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.
Return a function that takes a specific instance.
Pass each state through the adapters sequentially.
Make sure an instance is returned.
Core
Every instance must have a process and be callable.
Config
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 // falseAllow 10000 iterations by default
return new S(null).config.iterations // 10000Run until the return symbol is present by default.
return new S(null).config.until({ [Return]: undefined }) // trueDo not keep the stack trace by default
return new S(null).config.trace // falseShallow merge changes by default
return new S(null).config.deep // falseDo not override the execution method by default
return new S(null).config.override // nullUses 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 // [ ]S._closest (instance, path = [], ...nodeTypes)
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...
Use get_closest_path to find the closest path.
Get the type of the node
Pick this node if it matches any of the given types
S._changes (instance, state = {}, changes = {})
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' } }If the strict state flag is truthy, perform state checking logic
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.
If the strict state flag is set to the Strict Types Symbol, perform type checking logic.
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
S._proceed (instance, state = {}, node = undefined)
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 // trueIf the node is unrecognised, throw a TypeEror
return S._proceed(
new S(null),
{[Stack]:[{path:[],origin:Return,point:0}]},
false
) // NodeTypeErrorCall 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' }S._perform (instance, state = {}, action = undefined)
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 // trueIf the given action is not recognised, throw a NodeTypeError
return S._perform(
new S(null),
{},
false
) // NodeTypeErrorPerforms 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 // trueIf the given node is not recognised, throw a NodeTypeError
return S._execute(
new S(null),
{},
false
) // NodeTypeErrorExecute 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'S._traverse(instance, iterator)
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' } ] }Create an iteration function to be used recursively
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
S._run (instance, ...input)
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) // undefinedWill not handle promises in sync mode
const instance = new S(() => Promise.resolve({ [Return]: 'return value' }))
return S._run(instance) // undefinedIs the same as running the executable instance itself
const instance = new S({ [Return]: 'return value' })
return S._run(instance) === instance() // trueTakes 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) // trueExtract 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 }Turn the arguments into an initial condition
Runs the input adapter
let inputAdapterCalled;
S._run(
new S(null).output(ident).until(() => true)
.input((state) => {
inputAdapterCalled = true
return state
}),
{ }
)
return inputAdapterCalled // trueTakes 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 ]Merge the initial condition with the default initial state
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 // 0Before modifiers are called
let beforeAdapterCalled = false
S._run(
new S(null).output(ident).until(() => true)
.before((state) => {
beforeAdapterCalled = true
return state
}),
{ }
)
return beforeAdapterCalled // trueBefore 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 // trueInitial 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' }Uses the Stack from the initial state - allows for starting at arbitrary positions
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]: [ ] }Keep the return value if it already exists
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 } }Repeat for a limited number of iterations.
This should be fine for most finite machines, but may be too little for some constantly running machines.
Check the configured until condition to see if we should exit.
let untilCalled = false
S._run(new S(
() => ({ myProperty: 'myValue' })
).until(() => {
untilCalled = true
return true
}).output(ident), {
[Stack]: [{path:[],origin:Return,point:0}],
})
return untilCalled // trueDo 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]: [],
}) // MaxIterationsErrorIf 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 // trueDefault Nodes
Error Node
Throws the given error
The ErrorN symbol is exported as { ErrorN }
import { ErrorN } from './index.js'
return ErrorN; // successThis definition is exported by the library as { ErrorNode }
import { ErrorNode } from './index.js'
return ErrorNode; // successUses the ErrorN symbol as the type.
return ErrorNode.type // Symbol(SSSM Error)Look for Error objects, or Error constructors.
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)Perform an error by throwing it, no fancy magic.
Throw an error constructed by the function.
return ErrorNode.perform(TestError, {}) // TestErrorThrow an existing error instance.
return ErrorNode.perform(new TestError(), {}) // TestErrorChanges Node
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; // successThis definition is exported by the library as { ChangesNode }
import { ChangesNode } from './index.js'
return ChangesNode; // successUses 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' } }Sequence 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' }),
]).output(({ result }) => result)
return instance({ result: 'start' }) // 'start addition1 addition2'The Sequence symbol is exported as { Sequence }
import { Sequence } from './index.js'
return Sequence; // successThis definition is exported by the library as { SequenceNode }
import { SequenceNode } from './index.js'
return SequenceNode; // successUses the Sequence symbol as the type.
return SequenceNode.type // Symbol(SSSM Sequence)Proceed by running the next node in the sequence
Get the current index in this sequence from the path
If there are more nodes to execute
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.
Function Node
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; // successThis definition is exported by the library as { FunctionNode }
import { FunctionNode } from './index.js'
return FunctionNode; // successUses 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 