diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/state_machine.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/state_machine.test.ts index f6e1f8fba5a20..795440025c7fd 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/state_machine.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/state_machine.test.ts @@ -10,29 +10,40 @@ import { appContextService } from '../../..'; import { handleState } from './state_machine'; -const getTestDefinition = ( - mockOnTransition1: any, - mockOnTransition2: any, - mockOnTransition3: any, - context?: any, - onPostTransition?: any -) => { +const getTestDefinition = ({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + mockPostTransition, + mockPreTransition, +}: { + mockOnTransition1: any; + mockOnTransition2: any; + mockOnTransition3: any; + context?: any; + mockPostTransition?: any; + mockPreTransition?: any; +}) => { return { context, states: { state1: { + onPreTransition: mockPreTransition, onTransition: mockOnTransition1, - onPostTransition, + onPostTransition: mockPostTransition, nextState: 'state2', }, state2: { + onPreTransition: mockPreTransition, onTransition: mockOnTransition2, - onPostTransition, + onPostTransition: mockPostTransition, nextState: 'state3', }, state3: { + onPreTransition: mockPreTransition, onTransition: mockOnTransition3, - onPostTransition, + onPostTransition: mockPostTransition, nextState: 'end', }, }, @@ -52,19 +63,19 @@ describe('handleState', () => { }); it('should execute all the state machine transitions based on the provided data structure', async () => { - const mockOnTransitionState1 = jest.fn(); - const mockOnTransitionState2 = jest.fn(); - const mockOnTransitionState3 = jest.fn(); - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3 - ); + const mockOnTransition1 = jest.fn(); + const mockOnTransition2 = jest.fn(); + const mockOnTransition3 = jest.fn(); + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + }); await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalledTimes(1); - expect(mockOnTransitionState2).toHaveBeenCalledTimes(1); - expect(mockOnTransitionState3).toHaveBeenCalledTimes(1); + expect(mockOnTransition1).toHaveBeenCalledTimes(1); + expect(mockOnTransition2).toHaveBeenCalledTimes(1); + expect(mockOnTransition3).toHaveBeenCalledTimes(1); expect(mockContract.logger?.debug).toHaveBeenCalledWith( 'Executed state: state1 with status: success - nextState: state2' ); @@ -77,22 +88,22 @@ describe('handleState', () => { }); it('should call the onTransition function with context data and the return value is saved for the next iteration', async () => { - const mockOnTransitionState1 = jest.fn().mockReturnValue({ arrayData: ['test1', 'test2'] }); - const mockOnTransitionState2 = jest + const mockOnTransition1 = jest.fn().mockReturnValue({ arrayData: ['test1', 'test2'] }); + const mockOnTransition2 = jest .fn() .mockImplementation(() => Promise.resolve({ promiseData: {} })); - const mockOnTransitionState3 = jest.fn().mockReturnValue({ lastData: ['test3'] }); - const contextData = { testData: 'test' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData - ); + const mockOnTransition3 = jest.fn().mockReturnValue({ lastData: ['test3'] }); + const context = { testData: 'test' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + }); await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalledWith({ testData: 'test' }); - expect(mockOnTransitionState2).toHaveBeenCalledWith( + expect(mockOnTransition1).toHaveBeenCalledWith({ testData: 'test' }); + expect(mockOnTransition2).toHaveBeenCalledWith( expect.objectContaining({ testData: 'test', arrayData: ['test1', 'test2'], @@ -102,7 +113,7 @@ describe('handleState', () => { }, }) ); - expect(mockOnTransitionState3).toHaveBeenCalledWith( + expect(mockOnTransition3).toHaveBeenCalledWith( expect.objectContaining({ testData: 'test', arrayData: ['test1', 'test2'], @@ -126,27 +137,27 @@ describe('handleState', () => { }); it('should save the return data from transitions also when return type is function', async () => { - const mockOnTransitionState1 = jest.fn().mockReturnValue({ arrayData: ['test1', 'test2'] }); + const mockOnTransition1 = jest.fn().mockReturnValue({ arrayData: ['test1', 'test2'] }); const state2Result = () => { return { result: 'test', }; }; - const mockOnTransitionState2 = jest.fn().mockImplementation(() => { + const mockOnTransition2 = jest.fn().mockImplementation(() => { return state2Result; }); - const mockOnTransitionState3 = jest.fn(); - const contextData = { testData: 'test' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData - ); + const mockOnTransition3 = jest.fn(); + const context = { testData: 'test' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + }); await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalledWith({ testData: 'test' }); - expect(mockOnTransitionState2).toHaveBeenCalledWith( + expect(mockOnTransition1).toHaveBeenCalledWith({ testData: 'test' }); + expect(mockOnTransition2).toHaveBeenCalledWith( expect.objectContaining({ testData: 'test', arrayData: ['test1', 'test2'], @@ -157,7 +168,7 @@ describe('handleState', () => { }, }) ); - expect(mockOnTransitionState3).toHaveBeenCalledWith( + expect(mockOnTransition3).toHaveBeenCalledWith( expect.objectContaining({ testData: 'test', arrayData: ['test1', 'test2'], @@ -181,7 +192,7 @@ describe('handleState', () => { }); it('should return updated context data', async () => { - const mockOnTransitionState1 = jest + const mockOnTransition1 = jest .fn() .mockImplementation(() => Promise.resolve({ promiseData: {} })); const state2Result = () => { @@ -189,21 +200,21 @@ describe('handleState', () => { result: 'test', }; }; - const mockOnTransitionState2 = jest.fn().mockImplementation(() => { + const mockOnTransition2 = jest.fn().mockImplementation(() => { return state2Result; }); - const mockOnTransitionState3 = jest.fn().mockReturnValue({ lastData: ['test3'] }); - const contextData = { testData: 'test' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData - ); + const mockOnTransition3 = jest.fn().mockReturnValue({ lastData: ['test3'] }); + const context = { testData: 'test' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + }); const updatedContext = await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalledWith({ testData: 'test' }); - expect(mockOnTransitionState2).toHaveBeenCalledWith( + expect(mockOnTransition1).toHaveBeenCalledWith({ testData: 'test' }); + expect(mockOnTransition2).toHaveBeenCalledWith( expect.objectContaining({ testData: 'test', promiseData: {}, @@ -214,7 +225,7 @@ describe('handleState', () => { }, }) ); - expect(mockOnTransitionState3).toHaveBeenCalledWith( + expect(mockOnTransition3).toHaveBeenCalledWith( expect.objectContaining({ testData: 'test', promiseData: {}, @@ -241,22 +252,22 @@ describe('handleState', () => { }); it('should update a variable in the context at every call and return the updated value', async () => { - const mockOnTransitionState1 = jest.fn().mockReturnValue({ runningVal: 'test1' }); - const mockOnTransitionState2 = jest + const mockOnTransition1 = jest.fn().mockReturnValue({ runningVal: 'test1' }); + const mockOnTransition2 = jest .fn() .mockImplementation(() => Promise.resolve({ runningVal: 'test2' })); - const mockOnTransitionState3 = jest.fn().mockReturnValue({ runningVal: 'test3' }); - const contextData = { runningVal: [], fixedVal: 'something' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData - ); + const mockOnTransition3 = jest.fn().mockReturnValue({ runningVal: 'test3' }); + const context = { runningVal: [], fixedVal: 'something' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + }); const updatedContext = await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalledWith({ runningVal: [], fixedVal: 'something' }); - expect(mockOnTransitionState2).toHaveBeenCalledWith( + expect(mockOnTransition1).toHaveBeenCalledWith({ runningVal: [], fixedVal: 'something' }); + expect(mockOnTransition2).toHaveBeenCalledWith( expect.objectContaining({ runningVal: 'test1', fixedVal: 'something', @@ -266,7 +277,7 @@ describe('handleState', () => { }, }) ); - expect(mockOnTransitionState3).toHaveBeenCalledWith( + expect(mockOnTransition3).toHaveBeenCalledWith( expect.objectContaining({ runningVal: 'test2', fixedVal: 'something', @@ -289,29 +300,29 @@ describe('handleState', () => { }); it('should execute the transition starting from the provided state', async () => { - const mockOnTransitionState1 = jest.fn().mockReturnValue({ runningVal: 'test1' }); - const mockOnTransitionState2 = jest + const mockOnTransition1 = jest.fn().mockReturnValue({ runningVal: 'test1' }); + const mockOnTransition2 = jest .fn() .mockImplementation(() => Promise.resolve({ runningVal: 'test2' })); - const mockOnTransitionState3 = jest.fn().mockReturnValue({ runningVal: 'test3' }); - const contextData = { runningVal: [], fixedVal: 'something' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData - ); + const mockOnTransition3 = jest.fn().mockReturnValue({ runningVal: 'test3' }); + const context = { runningVal: [], fixedVal: 'something' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + }); const updatedContext = await handleState('state2', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalledTimes(0); - expect(mockOnTransitionState2).toHaveBeenCalledWith( + expect(mockOnTransition1).toHaveBeenCalledTimes(0); + expect(mockOnTransition2).toHaveBeenCalledWith( expect.objectContaining({ runningVal: [], fixedVal: 'something', }) ); - expect(mockOnTransitionState3).toHaveBeenCalledWith( + expect(mockOnTransition3).toHaveBeenCalledWith( expect.objectContaining({ runningVal: 'test2', fixedVal: 'something', @@ -335,46 +346,121 @@ describe('handleState', () => { it('should throw and return updated context with latest error when a state returns error', async () => { const error = new Error('Installation failed'); - const mockOnTransitionState1 = jest.fn().mockRejectedValue(error); - const mockOnTransitionState2 = jest.fn(); - const mockOnTransitionState3 = jest.fn(); - const contextData = { fixedVal: 'something' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData - ); + const mockOnTransition1 = jest.fn().mockRejectedValue(error); + const mockOnTransition2 = jest.fn(); + const mockOnTransition3 = jest.fn(); + const context = { fixedVal: 'something' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + }); const promise = handleState('state1', testDefinition, testDefinition.context); await expect(promise).rejects.toThrowError('Installation failed'); - expect(mockOnTransitionState1).toHaveBeenCalledTimes(1); - expect(mockOnTransitionState2).toHaveBeenCalledTimes(0); - expect(mockOnTransitionState3).toHaveBeenCalledTimes(0); + expect(mockOnTransition1).toHaveBeenCalledTimes(1); + expect(mockOnTransition2).toHaveBeenCalledTimes(0); + expect(mockOnTransition3).toHaveBeenCalledTimes(0); expect(mockContract.logger?.warn).toHaveBeenCalledWith( 'Error during execution of state "state1" with status "failed": Installation failed' ); }); + it('should execute preTransition function before the transition gets executed', async () => { + const mockOnTransition1 = jest.fn(); + const mockOnTransition2 = jest.fn(); + const mockOnTransition3 = jest.fn(); + const mockPreTransition = jest.fn(); + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + mockPreTransition, + }); + await handleState('state1', testDefinition, testDefinition.context); + + expect(mockPreTransition).toHaveBeenCalled(); + expect(mockOnTransition1).toHaveBeenCalled(); + + expect(mockContract.logger?.debug).toHaveBeenCalledWith( + 'Executing pre transition function: mockConstructor' + ); + }); + + it('should execute preTransition function before the transition gets executed passing the updated context', async () => { + const mockPreTransition = jest.fn().mockReturnValue({ runningVal: 'test1' }); + const mockOnTransition1 = jest.fn(); + const mockOnTransition2 = jest + .fn() + .mockImplementation(() => Promise.resolve({ runningVal: 'test2' })); + const mockOnTransition3 = jest.fn().mockReturnValue({ runningVal: 'test3' }); + const context = { fixedVal: 'something' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + mockPreTransition, + context, + }); + const updatedContext = await handleState('state1', testDefinition, testDefinition.context); + + expect(updatedContext).toEqual( + expect.objectContaining({ + fixedVal: 'something', + runningVal: 'test3', + latestExecutedState: { + name: 'state3', + started_at: expect.anything(), + }, + }) + ); + }); + + it('should throw error and not execute subsequent transitions when onPreTransition throws error', async () => { + const error = new Error('Precondition failed'); + const mockPreTransition = jest.fn().mockRejectedValue(error); + const mockOnTransition1 = jest.fn(); + const mockOnTransition2 = jest.fn(); + + const mockOnTransition3 = jest.fn(); + const context = { fixedVal: 'something' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + mockPreTransition, + context, + }); + + await expect( + handleState('state1', testDefinition, testDefinition.context) + ).rejects.toThrowError('Precondition failed'); + + expect(mockPreTransition).toHaveBeenCalled(); + expect(mockOnTransition1).not.toHaveBeenCalled(); + expect(mockOnTransition2).not.toHaveBeenCalled(); + expect(mockOnTransition3).not.toHaveBeenCalled(); + }); + it('should execute postTransition function after the transition is complete', async () => { - const mockOnTransitionState1 = jest.fn(); - const mockOnTransitionState2 = jest.fn(); - const mockOnTransitionState3 = jest.fn(); + const mockOnTransition1 = jest.fn(); + const mockOnTransition2 = jest.fn(); + const mockOnTransition3 = jest.fn(); const mockPostTransition = jest.fn(); - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - undefined, - mockPostTransition - ); + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + mockPostTransition, + }); await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalled(); + expect(mockOnTransition1).toHaveBeenCalled(); expect(mockPostTransition).toHaveBeenCalled(); - expect(mockOnTransitionState2).toHaveBeenCalled(); + expect(mockOnTransition2).toHaveBeenCalled(); expect(mockPostTransition).toHaveBeenCalled(); - expect(mockOnTransitionState3).toHaveBeenCalled(); + expect(mockOnTransition3).toHaveBeenCalled(); expect(mockPostTransition).toHaveBeenCalled(); expect(mockContract.logger?.debug).toHaveBeenCalledWith( 'Executing post transition function: mockConstructor' @@ -382,27 +468,27 @@ describe('handleState', () => { }); it('should execute postTransition function after the transition passing the updated context', async () => { - const mockOnTransitionState1 = jest.fn().mockReturnValue({ runningVal: 'test1' }); - const mockOnTransitionState2 = jest + const mockOnTransition1 = jest.fn().mockReturnValue({ runningVal: 'test1' }); + const mockOnTransition2 = jest .fn() .mockImplementation(() => Promise.resolve({ runningVal: 'test2' })); - const mockOnTransitionState3 = jest.fn().mockReturnValue({ runningVal: 'test3' }); + const mockOnTransition3 = jest.fn().mockReturnValue({ runningVal: 'test3' }); const mockPostTransition = jest.fn(); - const contextData = { fixedVal: 'something' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData, - mockPostTransition - ); + const context = { fixedVal: 'something' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + mockPostTransition, + }); const updatedContext = await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalled(); + expect(mockOnTransition1).toHaveBeenCalled(); expect(mockPostTransition).toHaveBeenCalled(); - expect(mockOnTransitionState2).toHaveBeenCalled(); + expect(mockOnTransition2).toHaveBeenCalled(); expect(mockPostTransition).toHaveBeenCalled(); - expect(mockOnTransitionState3).toHaveBeenCalled(); + expect(mockOnTransition3).toHaveBeenCalled(); expect(updatedContext).toEqual( expect.objectContaining({ fixedVal: 'something', @@ -421,22 +507,22 @@ describe('handleState', () => { it('should execute postTransition correctly also when a transition throws', async () => { const error = new Error('Installation failed'); - const mockOnTransitionState1 = jest.fn().mockReturnValue({ result1: 'test' }); - const mockOnTransitionState2 = jest.fn().mockRejectedValue(error); - const mockOnTransitionState3 = jest.fn(); + const mockOnTransition1 = jest.fn().mockReturnValue({ result1: 'test' }); + const mockOnTransition2 = jest.fn().mockRejectedValue(error); + const mockOnTransition3 = jest.fn(); const mockPostTransition = jest.fn(); - const contextData = { testData: 'test' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData, - mockPostTransition - ); + const context = { testData: 'test' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + mockPostTransition, + }); const promise = handleState('state1', testDefinition, testDefinition.context); await expect(promise).rejects.toThrowError('Installation failed'); - expect(mockOnTransitionState1).toHaveBeenCalledTimes(1); + expect(mockOnTransition1).toHaveBeenCalledTimes(1); expect(mockPostTransition).toHaveBeenCalledWith( expect.objectContaining({ result1: 'test', @@ -449,7 +535,7 @@ describe('handleState', () => { }, }) ); - expect(mockOnTransitionState2).toHaveBeenCalledTimes(1); + expect(mockOnTransition2).toHaveBeenCalledTimes(1); expect(mockPostTransition).toHaveBeenCalledWith( expect.objectContaining({ result1: 'test', @@ -460,26 +546,26 @@ describe('handleState', () => { }, }) ); - expect(mockOnTransitionState3).toHaveBeenCalledTimes(0); + expect(mockOnTransition3).toHaveBeenCalledTimes(0); }); it('should log a warning when postTransition exits with errors and continue executing the states', async () => { const error = new Error('Installation failed'); - const mockOnTransitionState1 = jest.fn().mockReturnValue({ result1: 'test' }); - const mockOnTransitionState2 = jest.fn(); - const mockOnTransitionState3 = jest.fn(); + const mockOnTransition1 = jest.fn().mockReturnValue({ result1: 'test' }); + const mockOnTransition2 = jest.fn(); + const mockOnTransition3 = jest.fn(); const mockPostTransition = jest.fn().mockRejectedValue(error); - const contextData = { testData: 'test' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData, - mockPostTransition - ); + const context = { testData: 'test' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + mockPostTransition, + }); const updatedContext = await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalledTimes(1); + expect(mockOnTransition1).toHaveBeenCalledTimes(1); expect(mockPostTransition).toHaveBeenCalledWith( expect.objectContaining({ result1: 'test', @@ -490,8 +576,8 @@ describe('handleState', () => { }, }) ); - expect(mockOnTransitionState2).toHaveBeenCalledTimes(1); - expect(mockOnTransitionState3).toHaveBeenCalledTimes(1); + expect(mockOnTransition2).toHaveBeenCalledTimes(1); + expect(mockOnTransition3).toHaveBeenCalledTimes(1); expect(mockContract.logger?.warn).toHaveBeenCalledWith( 'Error during execution of post transition function: Installation failed' ); @@ -509,21 +595,21 @@ describe('handleState', () => { }); it('should exit and log a warning when the provided OnTransition is not a function', async () => { - const mockOnTransitionState1 = jest.fn().mockReturnValue({ result1: 'test' }); - const mockOnTransitionState2 = undefined; - const mockOnTransitionState3 = jest.fn(); - - const contextData = { testData: 'test' }; - const testDefinition = getTestDefinition( - mockOnTransitionState1, - mockOnTransitionState2, - mockOnTransitionState3, - contextData - ); + const mockOnTransition1 = jest.fn().mockReturnValue({ result1: 'test' }); + const mockOnTransition2 = undefined; + const mockOnTransition3 = jest.fn(); + + const context = { testData: 'test' }; + const testDefinition = getTestDefinition({ + mockOnTransition1, + mockOnTransition2, + mockOnTransition3, + context, + }); const updatedContext = await handleState('state1', testDefinition, testDefinition.context); - expect(mockOnTransitionState1).toHaveBeenCalledTimes(1); - expect(mockOnTransitionState3).toHaveBeenCalledTimes(0); + expect(mockOnTransition1).toHaveBeenCalledTimes(1); + expect(mockOnTransition3).toHaveBeenCalledTimes(0); expect(mockContract.logger?.warn).toHaveBeenCalledWith( 'Execution of state "state2" with status "failed": provided onTransition is not a valid function' ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/state_machine.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/state_machine.ts index c70a99e272361..4817dccc300a3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/state_machine.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/state_machine.ts @@ -13,6 +13,7 @@ export interface State { onTransition: any; nextState?: string; currentStatus?: string; + onPreTransition?: any; onPostTransition?: any; } @@ -24,16 +25,19 @@ export type StateMachineStates = Record; * context: {}, * states: { * state1: { + * onPreTransition: onPreTransition, * onTransition: onState1Transition, * onPostTransition: onPostTransition, * nextState: 'state2', * }, * state2: { + * onPreTransition: onPreTransition, * onTransition: onState2Transition, * onPostTransition: onPostTransition,, * nextState: 'state3', * }, * state3: { + * onPreTransition: onPreTransition, * onTransition: onState3Transition, * onPostTransition: onPostTransition, * nextState: 'end', @@ -63,6 +67,10 @@ export async function handleState( let currentStatus = 'pending'; let stateResult; let updatedContext = { ...context }; + + // execute pre transition function, if available + await executePreTransition(logger, updatedContext, currentState); + if (typeof currentState.onTransition === 'function') { logger.debug( `Current state ${currentStateName}: running transition ${currentState.onTransition.name}` @@ -123,6 +131,9 @@ export async function handleState( } } +/* + * executePostTransition: function that gets executed after the execution of any step, when defined + */ async function executePostTransition( logger: Logger, updatedContext: StateContext, @@ -137,3 +148,24 @@ async function executePostTransition( } } } + +/* + * executePreTransition: function that gets executed before the execution of any step, when defined + */ +async function executePreTransition( + logger: Logger, + updatedContext: StateContext, + currentState: State +) { + if (typeof currentState.onPreTransition === 'function') { + try { + await currentState.onPreTransition.call(undefined, updatedContext); + logger.debug(`Executing pre transition function: ${currentState.onPreTransition.name}`); + } catch (error) { + logger.warn(`Error during execution of pre transition function: ${error.message}`); + + // bubble up the error; if something goes wrong in the precondition we want to see what happened + throw error; + } + } +}