Skip to content

Commit f38d729

Browse files
authored
Merge pull request #31 from nibble-4bits/refactor-5
Refactor 5
2 parents 0f5dfb9 + 0b69721 commit f38d729

14 files changed

+218
-173
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const stateMachine = new StateMachine(machineDefinition, { checkPaths: false });
8383

8484
### `async StateMachine.run(input[, options])`
8585

86-
Executes the state machine with the given `input` parameter and returns the result of the execution.
86+
Runs the state machine with the given `input` parameter and returns the result of the execution. Each execution is independent of all others, meaning that you can concurrently call this method as many times as needed, without worrying about race conditions.
8787

8888
It takes the following parameters:
8989

src/StateMachine.ts

+151-125
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { ChoiceState } from './typings/ChoiceState';
99
import type { SucceedState } from './typings/SucceedState';
1010
import type { FailState } from './typings/FailState';
1111
import type { RunOptions, StateHandler, ValidationOptions } from './typings/StateMachineImplementation';
12+
import type { ExecutionResult } from './typings/StateHandlers';
1213
import { TaskStateHandler } from './stateHandlers/TaskStateHandler';
1314
import { MapStateHandler } from './stateHandlers/MapStateHandler';
1415
import { PassStateHandler } from './stateHandlers/PassStateHandler';
@@ -23,47 +24,18 @@ import {
2324
processResultPath,
2425
} from './InputOutputProcessing';
2526
import aslValidator from 'asl-validator';
27+
import cloneDeep from 'lodash/cloneDeep.js';
2628

2729
export class StateMachine {
2830
/**
29-
* The name of the state currently being executed.
31+
* The structure of the State Machine as represented by the Amazon States Language.
3032
*/
31-
private currStateName: string;
33+
private readonly definition: StateMachineDefinition;
3234

3335
/**
34-
* The current state being executed.
36+
* A map of functions to execute each type of state.
3537
*/
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;
6739

6840
/**
6941
* Options to control whether to apply certain validations to the state machine definition.
@@ -87,21 +59,15 @@ export class StateMachine {
8759
throw new Error(`State machine definition is invalid, see error(s) below:\n ${errorsText('\n')}`);
8860
}
8961

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,
10571
};
10672
this.validationOptions = validationOptions;
10773
}
@@ -112,64 +78,79 @@ export class StateMachine {
11278
* @param options Miscellaneous options to control certain behaviors of the execution.
11379
*/
11480
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 = '';
11887
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> = {};
12790

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;
138106
} while (!isEndState);
139107

140-
return this.currResult;
108+
return currResult;
141109
}
142110

143111
/**
144112
* Process the current input according to the `InputPath` and `Parameters` fields.
145113
*/
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);
149119
}
150120

151-
if ('Parameters' in this.currState && this.currState.Type !== 'Map') {
121+
if ('Parameters' in currentState && currentState.Type !== 'Map') {
152122
// `Parameters` field is handled differently in the `Map` state,
153123
// 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);
155125
}
126+
127+
return processedInput;
156128
}
157129

158130
/**
159131
* Process the current result according to the `ResultSelector`, `ResultPath` and `OutputPath` fields.
160132
*/
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);
164143
}
165144

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);
168147
}
169148

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);
172151
}
152+
153+
return processedResult;
173154
}
174155

175156
/**
@@ -178,13 +159,19 @@ export class StateMachine {
178159
* Invokes the Lambda function specified in the `Resource` field
179160
* and sets the current result of the state machine to the value returned by the Lambda.
180161
*/
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;
188175
}
189176

190177
/**
@@ -194,14 +181,20 @@ export class StateMachine {
194181
* by the `ItemsPath` field, and then processes each item by passing it
195182
* as the input to the state machine specified in the `Iterator` field.
196183
*/
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, {
200193
validationOptions: this.validationOptions,
201194
runOptions: options,
202195
});
203196

204-
this.currResult = stateResult;
197+
return executionResult;
205198
}
206199

207200
/**
@@ -210,12 +203,19 @@ export class StateMachine {
210203
* If the `Result` field is specified, copies `Result` into the current result.
211204
* Else, copies the current input into the current result.
212205
*/
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;
219219
}
220220

221221
/**
@@ -224,15 +224,21 @@ export class StateMachine {
224224
* Pauses the state machine execution for a certain amount of time
225225
* based on one of the `Seconds`, `Timestamp`, `SecondsPath` or `TimestampPath` fields.
226226
*/
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, {
232238
waitTimeOverrideOption,
233239
});
234240

235-
this.currResult = stateResult;
241+
return executionResult;
236242
}
237243

238244
/**
@@ -249,38 +255,58 @@ export class StateMachine {
249255
* If no rule matches and the `Default` field is not specified, throws a
250256
* States.NoChoiceMatched error.
251257
*/
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;
259271
}
260272

261273
/**
262274
* Handler for succeed states.
263275
*
264276
* Ends the state machine execution successfully.
265277
*/
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;
272291
}
273292

274293
/**
275294
* Handler for fail states.
276295
*
277296
* Ends the state machine execution and marks it as a failure.
278297
*/
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;
285311
}
286312
}

0 commit comments

Comments
 (0)