const createMachine = require('./createMachine')
const { validateStateMachine } = require('./_internal')
/**
* Exends the base JavaScript base `Error`
* @typedef {Object<string, any>} TransitionError
* @property {string} name The name of the error (in this case "TransitionError")
* @property {string} data.currentState The current state of the state machine at the time the error was encountered
*/
class TransitionError extends Error {
constructor(message, state, additionalData = {}) {
super(message)
this.name = this.constructor.name
this.data = {
...(
typeof additionalData === 'object' && additionalData.constructor.name === 'Object'
? additionalData
: {}
),
currentState: state
}
}
}
/**
* Takes a given state machine and handler and returns a function which is ready to receive the starting "state" value _and_ any starting data, following through the entire sequence of async function until the state stops advancing.
* Note, a handler will receive the initial data, the starting "state" value and any (optional) dependencies to place into the context for the async functions contained in the handler.
* A handler doesn't have to be a function but can also be an object whose keys are possible "state" values and whose values are functions that receive the initial data, state machine, and context.
*
* @function
* @name createTransitionHandler
* @throws {TypeError} If a state machine is not provided
* @throws {TypeError} If a handler function/object is not provided
* @throws {TypeError} If the handler is not a function or an object whose keys do not correspond to keys in the state `machine`
* @throws {TypeError} If the state machine has any values which are not objects
* @throws {TypeError} If the state machine's "sub-objects" has values that are not keys on the root of the object
* @param {function|Object<string, function>} handler A handler function or an object of individual handler function (the keys on the object must match possible "states" otherwise they'll never be called)
* @param {Object<string, Object<string, string>>} machine An object whose keys are possible states and whose values are also objects (but whose keys are transition names and whose values are states they will advance to)
* @returns {function} A wrapped handler function that is ready to receive the (1) initial data, (2) starting "state" value on the machine, and (3) an object containing any dependencies the handler may have
*/
function createTransitionHandler(handler, machine) {
if (!machine) {
throw new TypeError('A state machine is required but was not provided')
}
if (!handler) {
throw new TypeError('A handler is required for the state machine transitions')
}
validateStateMachine(machine)
const getStateHandler = typeof handler === 'function'
? () => handler
: handler && typeof handler === 'object' && handler.constructor.name === 'Object'
? currentState => {
if (typeof handler[currentState] !== 'function') {
throw new TypeError(`No handler was defined for the current state of '${currentState}'`)
}
return handler[currentState]
} : undefined
if (!getStateHandler) {
throw new TypeError(
'handler must be a function or an object whose keys are state names and whose values are functions'
)
}
/**
* A handler function that will advance from state to state - executing each piece of logic and/or async function the user defined, until the state can no longer advance and the current value or error is returned at that point.
*
* @function
* @name wrappedTransitionHandler
* @throws {TransitionError} If the handler encounters an unexpected exception
* @param {Object<string, any>} initialData Any starting data for the handler
* @param {string} initialState A starting "state" value which should be one of the possible values on the state machine
* @param {Object<string, any>} [context] An optional object containing any dependencies of the handler (API clients, caches, etc.)
* @returns {*} When the handler can no longer advance to another possible state, the value of the last function it executed is returned
*/
return function wrappedTransitionHandler(initialData, initialState, context) {
let currentState
const nextState = createMachine(machine, initialState || 'initial')
function runNextTransition(r) {
if (r && /Error$/.test(r.constructor.name)) {
return Promise.reject(r)
} else if (currentState !== nextState()) {
currentState = nextState()
const response = getStateHandler(currentState)(r, nextState, context)
if (response != null && typeof response.then === 'function') {
return response.then(res => runNextTransition(res))
} else {
return runNextTransition(response)
}
} else {
return Promise.resolve(r)
}
}
return runNextTransition(initialData).catch(err => {
throw new TransitionError(
err.message || err.toString(),
nextState(), {
...((err.data && typeof err.data === 'object' && err.data) || {}),
...((err.extensions && typeof err.extensions === 'object' && err.extensions) || {})
}
)
})
}
}
module.exports = createTransitionHandler