@@ -9,6 +9,7 @@ import type { ChoiceState } from './typings/ChoiceState';
9
9
import type { SucceedState } from './typings/SucceedState' ;
10
10
import type { FailState } from './typings/FailState' ;
11
11
import type { RunOptions , StateHandler , ValidationOptions } from './typings/StateMachineImplementation' ;
12
+ import type { ExecutionResult } from './typings/StateHandlers' ;
12
13
import { TaskStateHandler } from './stateHandlers/TaskStateHandler' ;
13
14
import { MapStateHandler } from './stateHandlers/MapStateHandler' ;
14
15
import { PassStateHandler } from './stateHandlers/PassStateHandler' ;
@@ -23,47 +24,18 @@ import {
23
24
processResultPath ,
24
25
} from './InputOutputProcessing' ;
25
26
import aslValidator from 'asl-validator' ;
27
+ import cloneDeep from 'lodash/cloneDeep.js' ;
26
28
27
29
export class StateMachine {
28
30
/**
29
- * The name of the state currently being executed .
31
+ * The structure of the State Machine as represented by the Amazon States Language .
30
32
*/
31
- private currStateName : string ;
33
+ private readonly definition : StateMachineDefinition ;
32
34
33
35
/**
34
- * The current state being executed .
36
+ * A map of functions to execute each type of state .
35
37
*/
36
- private currState : AllStates ;
37
-
38
- /**
39
- * The unmodified input to the current state.
40
- */
41
- private rawInput : JSONValue ;
42
-
43
- /**
44
- * The input that can be modified according to the `InputPath` and `Parameters` fields of the current state.
45
- */
46
- private currInput : JSONValue ;
47
-
48
- /**
49
- * The result that can be modified according to the `ResultSelector`, `ResultPath` and `OutputPath` fields of the current state.
50
- */
51
- private currResult : JSONValue ;
52
-
53
- /**
54
- * The context object of the state machine.
55
- */
56
- private context : Record < string , unknown > ;
57
-
58
- /**
59
- * A map of all states defined in the state machine.
60
- */
61
- private readonly states : Record < string , AllStates > ;
62
-
63
- /**
64
- * A map of functions to handle each type of state.
65
- */
66
- private readonly stateHandlers : StateHandler ;
38
+ private readonly stateExecutors : StateHandler ;
67
39
68
40
/**
69
41
* Options to control whether to apply certain validations to the state machine definition.
@@ -87,21 +59,15 @@ export class StateMachine {
87
59
throw new Error ( `State machine definition is invalid, see error(s) below:\n ${ errorsText ( '\n' ) } ` ) ;
88
60
}
89
61
90
- this . states = definition . States ;
91
- this . currStateName = definition . StartAt ;
92
- this . currState = this . states [ this . currStateName ] ;
93
- this . rawInput = { } ;
94
- this . currInput = { } ;
95
- this . currResult = null ;
96
- this . context = { } ;
97
- this . stateHandlers = {
98
- Task : this . handleTaskState . bind ( this ) ,
99
- Map : this . handleMapState . bind ( this ) ,
100
- Pass : this . handlePassState . bind ( this ) ,
101
- Wait : this . handleWaitState . bind ( this ) ,
102
- Choice : this . handleChoiceState . bind ( this ) ,
103
- Succeed : this . handleSucceedState . bind ( this ) ,
104
- Fail : this . handleFailState . bind ( this ) ,
62
+ this . definition = definition ;
63
+ this . stateExecutors = {
64
+ Task : this . executeTaskState ,
65
+ Map : this . executeMapState ,
66
+ Pass : this . executePassState ,
67
+ Wait : this . executeWaitState ,
68
+ Choice : this . executeChoiceState ,
69
+ Succeed : this . executeSucceedState ,
70
+ Fail : this . executeFailState ,
105
71
} ;
106
72
this . validationOptions = validationOptions ;
107
73
}
@@ -112,64 +78,79 @@ export class StateMachine {
112
78
* @param options Miscellaneous options to control certain behaviors of the execution.
113
79
*/
114
80
async run ( input : JSONValue , options ?: RunOptions ) : Promise < JSONValue > {
115
- this . rawInput = input ;
116
- this . currInput = input ;
117
-
81
+ let currState = this . definition . States [ this . definition . StartAt ] ;
82
+ let currStateName = this . definition . StartAt ;
83
+ let rawInput = cloneDeep ( input ) ;
84
+ let currInput = cloneDeep ( input ) ;
85
+ let currResult : JSONValue = null ;
86
+ let nextState = '' ;
118
87
let isEndState = false ;
119
- do {
120
- this . currState = this . states [ this . currStateName ] ;
121
-
122
- this . processInput ( ) ;
123
-
124
- await this . stateHandlers [ this . currState . Type ] ( options ) ;
125
-
126
- this . processResult ( ) ;
88
+ // eslint-disable-next-line prefer-const
89
+ let context : Record < string , unknown > = { } ;
127
90
128
- this . rawInput = this . currResult ;
129
- this . currInput = this . currResult ;
130
-
131
- if ( 'Next' in this . currState ) {
132
- this . currStateName = this . currState . Next ;
133
- }
134
-
135
- if ( 'End' in this . currState || this . currState . Type === 'Succeed' || this . currState . Type === 'Fail' ) {
136
- isEndState = true ;
137
- }
91
+ do {
92
+ currInput = this . processInput ( currState , currInput , context ) ;
93
+ ( {
94
+ stateResult : currResult ,
95
+ nextState,
96
+ isEndState,
97
+ // @ts -expect-error Indexing `this.stateHandlers` by non-literal value produces a `never` type for the `stateDefinition` parameter of the handler being called
98
+ } = await this . stateExecutors [ currState . Type ] ( currState , currInput , context , currStateName , options ) ) ;
99
+ currResult = this . processResult ( currState , currResult , rawInput , context ) ;
100
+
101
+ rawInput = currResult ;
102
+ currInput = currResult ;
103
+
104
+ currState = this . definition . States [ nextState ] ;
105
+ currStateName = nextState ;
138
106
} while ( ! isEndState ) ;
139
107
140
- return this . currResult ;
108
+ return currResult ;
141
109
}
142
110
143
111
/**
144
112
* Process the current input according to the `InputPath` and `Parameters` fields.
145
113
*/
146
- private processInput ( ) : void {
147
- if ( 'InputPath' in this . currState ) {
148
- this . currInput = processInputPath ( this . currState . InputPath , this . currInput , this . context ) ;
114
+ private processInput ( currentState : AllStates , input : JSONValue , context : Record < string , unknown > ) : JSONValue {
115
+ let processedInput = input ;
116
+
117
+ if ( 'InputPath' in currentState ) {
118
+ processedInput = processInputPath ( currentState . InputPath , processedInput , context ) ;
149
119
}
150
120
151
- if ( 'Parameters' in this . currState && this . currState . Type !== 'Map' ) {
121
+ if ( 'Parameters' in currentState && currentState . Type !== 'Map' ) {
152
122
// `Parameters` field is handled differently in the `Map` state,
153
123
// hence why we omit processing it here.
154
- this . currInput = processPayloadTemplate ( this . currState . Parameters , this . currInput , this . context ) ;
124
+ processedInput = processPayloadTemplate ( currentState . Parameters , processedInput , context ) ;
155
125
}
126
+
127
+ return processedInput ;
156
128
}
157
129
158
130
/**
159
131
* Process the current result according to the `ResultSelector`, `ResultPath` and `OutputPath` fields.
160
132
*/
161
- private processResult ( ) : void {
162
- if ( 'ResultSelector' in this . currState ) {
163
- this . currResult = processPayloadTemplate ( this . currState . ResultSelector , this . currResult , this . context ) ;
133
+ private processResult (
134
+ currentState : AllStates ,
135
+ result : JSONValue ,
136
+ rawInput : JSONValue ,
137
+ context : Record < string , unknown >
138
+ ) : JSONValue {
139
+ let processedResult = result ;
140
+
141
+ if ( 'ResultSelector' in currentState ) {
142
+ processedResult = processPayloadTemplate ( currentState . ResultSelector , processedResult , context ) ;
164
143
}
165
144
166
- if ( 'ResultPath' in this . currState ) {
167
- this . currResult = processResultPath ( this . currState . ResultPath , this . rawInput , this . currResult ) ;
145
+ if ( 'ResultPath' in currentState ) {
146
+ processedResult = processResultPath ( currentState . ResultPath , rawInput , processedResult ) ;
168
147
}
169
148
170
- if ( 'OutputPath' in this . currState ) {
171
- this . currResult = processOutputPath ( this . currState . OutputPath , this . currResult , this . context ) ;
149
+ if ( 'OutputPath' in currentState ) {
150
+ processedResult = processOutputPath ( currentState . OutputPath , processedResult , context ) ;
172
151
}
152
+
153
+ return processedResult ;
173
154
}
174
155
175
156
/**
@@ -178,13 +159,19 @@ export class StateMachine {
178
159
* Invokes the Lambda function specified in the `Resource` field
179
160
* and sets the current result of the state machine to the value returned by the Lambda.
180
161
*/
181
- private async handleTaskState ( options ?: RunOptions ) : Promise < void > {
182
- const overrideFn = options ?. overrides ?. taskResourceLocalHandlers ?. [ this . currStateName ] ;
183
-
184
- const taskStateHandler = new TaskStateHandler ( this . currState as TaskState ) ;
185
- const { stateResult } = await taskStateHandler . executeState ( this . currInput , this . context , { overrideFn } ) ;
186
-
187
- this . currResult = stateResult ;
162
+ private async executeTaskState (
163
+ stateDefinition : TaskState ,
164
+ input : JSONValue ,
165
+ context : Record < string , unknown > ,
166
+ stateName : string ,
167
+ options ?: RunOptions
168
+ ) : Promise < ExecutionResult > {
169
+ const overrideFn = options ?. overrides ?. taskResourceLocalHandlers ?. [ stateName ] ;
170
+
171
+ const taskStateHandler = new TaskStateHandler ( stateDefinition ) ;
172
+ const executionResult = await taskStateHandler . executeState ( input , context , { overrideFn } ) ;
173
+
174
+ return executionResult ;
188
175
}
189
176
190
177
/**
@@ -194,14 +181,20 @@ export class StateMachine {
194
181
* by the `ItemsPath` field, and then processes each item by passing it
195
182
* as the input to the state machine specified in the `Iterator` field.
196
183
*/
197
- private async handleMapState ( options ?: RunOptions ) : Promise < void > {
198
- const mapStateHandler = new MapStateHandler ( this . currState as MapState ) ;
199
- const { stateResult } = await mapStateHandler . executeState ( this . currInput , this . context , {
184
+ private async executeMapState (
185
+ stateDefinition : MapState ,
186
+ input : JSONValue ,
187
+ context : Record < string , unknown > ,
188
+ stateName : string ,
189
+ options ?: RunOptions
190
+ ) : Promise < ExecutionResult > {
191
+ const mapStateHandler = new MapStateHandler ( stateDefinition ) ;
192
+ const executionResult = await mapStateHandler . executeState ( input , context , {
200
193
validationOptions : this . validationOptions ,
201
194
runOptions : options ,
202
195
} ) ;
203
196
204
- this . currResult = stateResult ;
197
+ return executionResult ;
205
198
}
206
199
207
200
/**
@@ -210,12 +203,19 @@ export class StateMachine {
210
203
* If the `Result` field is specified, copies `Result` into the current result.
211
204
* Else, copies the current input into the current result.
212
205
*/
213
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
214
- private async handlePassState ( options ?: RunOptions ) : Promise < void > {
215
- const passStateHandler = new PassStateHandler ( this . currState as PassState ) ;
216
- const { stateResult } = await passStateHandler . executeState ( this . currInput , this . context ) ;
217
-
218
- this . currResult = stateResult ;
206
+ private async executePassState (
207
+ stateDefinition : PassState ,
208
+ input : JSONValue ,
209
+ context : Record < string , unknown > ,
210
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
211
+ stateName : string ,
212
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
213
+ options ?: RunOptions
214
+ ) : Promise < ExecutionResult > {
215
+ const passStateHandler = new PassStateHandler ( stateDefinition ) ;
216
+ const executionResult = await passStateHandler . executeState ( input , context ) ;
217
+
218
+ return executionResult ;
219
219
}
220
220
221
221
/**
@@ -224,15 +224,21 @@ export class StateMachine {
224
224
* Pauses the state machine execution for a certain amount of time
225
225
* based on one of the `Seconds`, `Timestamp`, `SecondsPath` or `TimestampPath` fields.
226
226
*/
227
- private async handleWaitState ( options ?: RunOptions ) : Promise < void > {
228
- const waitTimeOverrideOption = options ?. overrides ?. waitTimeOverrides ?. [ this . currStateName ] ;
229
-
230
- const waitStateHandler = new WaitStateHandler ( this . currState as WaitState ) ;
231
- const { stateResult } = await waitStateHandler . executeState ( this . currInput , this . context , {
227
+ private async executeWaitState (
228
+ stateDefinition : WaitState ,
229
+ input : JSONValue ,
230
+ context : Record < string , unknown > ,
231
+ stateName : string ,
232
+ options ?: RunOptions
233
+ ) : Promise < ExecutionResult > {
234
+ const waitTimeOverrideOption = options ?. overrides ?. waitTimeOverrides ?. [ stateName ] ;
235
+
236
+ const waitStateHandler = new WaitStateHandler ( stateDefinition ) ;
237
+ const executionResult = await waitStateHandler . executeState ( input , context , {
232
238
waitTimeOverrideOption,
233
239
} ) ;
234
240
235
- this . currResult = stateResult ;
241
+ return executionResult ;
236
242
}
237
243
238
244
/**
@@ -249,38 +255,58 @@ export class StateMachine {
249
255
* If no rule matches and the `Default` field is not specified, throws a
250
256
* States.NoChoiceMatched error.
251
257
*/
252
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
253
- private async handleChoiceState ( options ?: RunOptions ) : Promise < void > {
254
- const choiceStateHandler = new ChoiceStateHandler ( this . currState as ChoiceState ) ;
255
- const { stateResult, nextState } = await choiceStateHandler . executeState ( this . currInput , this . context ) ;
256
-
257
- this . currResult = stateResult ;
258
- this . currStateName = nextState ! ;
258
+ private async executeChoiceState (
259
+ stateDefinition : ChoiceState ,
260
+ input : JSONValue ,
261
+ context : Record < string , unknown > ,
262
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
263
+ stateName : string ,
264
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
265
+ options ?: RunOptions
266
+ ) : Promise < ExecutionResult > {
267
+ const choiceStateHandler = new ChoiceStateHandler ( stateDefinition ) ;
268
+ const executionResult = await choiceStateHandler . executeState ( input , context ) ;
269
+
270
+ return executionResult ;
259
271
}
260
272
261
273
/**
262
274
* Handler for succeed states.
263
275
*
264
276
* Ends the state machine execution successfully.
265
277
*/
266
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
267
- private async handleSucceedState ( options ?: RunOptions ) : Promise < void > {
268
- const succeedStateHandler = new SucceedStateHandler ( this . currState as SucceedState ) ;
269
- const { stateResult } = await succeedStateHandler . executeState ( this . currInput , this . context ) ;
270
-
271
- this . currResult = stateResult ;
278
+ private async executeSucceedState (
279
+ stateDefinition : SucceedState ,
280
+ input : JSONValue ,
281
+ context : Record < string , unknown > ,
282
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
283
+ stateName : string ,
284
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
285
+ options ?: RunOptions
286
+ ) : Promise < ExecutionResult > {
287
+ const succeedStateHandler = new SucceedStateHandler ( stateDefinition ) ;
288
+ const executionResult = await succeedStateHandler . executeState ( input , context ) ;
289
+
290
+ return executionResult ;
272
291
}
273
292
274
293
/**
275
294
* Handler for fail states.
276
295
*
277
296
* Ends the state machine execution and marks it as a failure.
278
297
*/
279
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
280
- private async handleFailState ( options ?: RunOptions ) : Promise < void > {
281
- const failStateHandler = new FailStateHandler ( this . currState as FailState ) ;
282
- const { stateResult } = await failStateHandler . executeState ( this . currInput , this . context ) ;
283
-
284
- this . currResult = stateResult ;
298
+ private async executeFailState (
299
+ stateDefinition : FailState ,
300
+ input : JSONValue ,
301
+ context : Record < string , unknown > ,
302
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
303
+ stateName : string ,
304
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
305
+ options ?: RunOptions
306
+ ) : Promise < ExecutionResult > {
307
+ const failStateHandler = new FailStateHandler ( stateDefinition ) ;
308
+ const executionResult = await failStateHandler . executeState ( input , context ) ;
309
+
310
+ return executionResult ;
285
311
}
286
312
}
0 commit comments