npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oversword/super-small-state-machine

v5.2.1

Published

A tiny, stand-alone state machine executor that uses an extremely simple syntax

Downloads

2

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() // 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

Instance

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() // 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

The 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 // 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

instance.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() // 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

instance.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() // StateReferenceError
const instance = new S(() => ({ unknownVariable: false}))
	.defaults({ knownVariable: true })
	.strict
	.unstrict
return instance() // Succeeds

instance.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() // Succeeds
const instance = new S(() => ({ unknownVariable: false}))
	.defaults({ knownVariable: true })
	.strict
return instance() // StateReferenceError

instance.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() // StateTypeError

instance.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() // MaxIterationsError

instance.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 // Infinity

instance.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; // 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

Chain

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) // 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

S.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() // 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

S.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() // 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

S.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() // StateTypeError

S.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() // MaxIterationsError

S.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 // Infinity

S.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 // 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 // [  ]

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 // 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' }

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 // 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'

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) // 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 }

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 // 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 ]

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 // 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' }

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 // 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

Default Nodes

Error Node

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)

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, {}) // TestError

Throw an existing error instance.

return ErrorNode.perform(new TestError(), {}) // TestError

Changes 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; // 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' } }

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; // 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)

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; // 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