Skip to content

Commit 2d9ffaf

Browse files
authored
Merge pull request #33 from nibble-4bits/feature/abort-execution
Feature/abort execution
2 parents f38d729 + 6136218 commit 2d9ffaf

14 files changed

+307
-110
lines changed

README.md

+63-13
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This package lets you run AWS Step Functions locally on your machine!
1616
- [ES Module](#es-module)
1717
- [API](#api)
1818
- [Constructor](#constructor-new-statemachinedefinition-validationoptions)
19-
- [StateMachine.run](#async-statemachineruninput-options)
19+
- [StateMachine.run](#statemachineruninput-options)
2020
- [License](#license)
2121

2222
## Features
@@ -51,6 +51,8 @@ import { StateMachine } from 'aws-local-stepfunctions';
5151

5252
### Constructor: `new StateMachine(definition[, validationOptions])`
5353

54+
#### Parameters
55+
5456
The constructor takes the following parameters:
5557

5658
- `definition`: The Amazon States Language definition of the state machine.
@@ -60,7 +62,7 @@ The constructor takes the following parameters:
6062

6163
The constructor will attempt to validate the definition by default, unless the `validationOptions` param is specified. If the definition is not valid, an error will be thrown.
6264

63-
Example:
65+
#### Example
6466

6567
```js
6668
import { StateMachine } from 'aws-local-stepfunctions';
@@ -81,19 +83,27 @@ const machineDefinition = {
8183
const stateMachine = new StateMachine(machineDefinition, { checkPaths: false });
8284
```
8385

84-
### `async StateMachine.run(input[, options])`
86+
### `StateMachine.run(input[, options])`
87+
88+
Runs the state machine with the given `input` parameter and returns an object with the following properties:
8589

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.
90+
- `abort`: A function that takes no parameters and doesn't return any value. If called, aborts the execution and throws an `ExecutionAbortedError`, unless the `noThrowOnAbort` option is set.
91+
- `result`: A `Promise` that resolves with the execution result once it finishes.
8792

88-
It takes the following parameters:
93+
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.
94+
95+
#### Parameters
8996

9097
- `input`: The initial input to pass to the state machine. This can be any valid JSON value.
9198
- `options` (optional):
92-
- `overrides`: An object to overrides the behavior of certain states:
99+
- `overrides`: An object to override the behavior of certain states:
93100
- `taskResourceLocalHandlers`: Overrides the resource of the specified `Task` states to run a local function.
94101
- `waitTimeOverrides`: Overrides the wait duration of the specified `Wait` states. The specifed override duration should be in milliseconds.
102+
- `noThrowOnAbort`: If this option is set to `true`, aborting the execution will simply return `null` as result instead of throwing.
103+
104+
#### Examples
95105

96-
Example without `options`:
106+
##### Example without `options`:
97107

98108
```js
99109
import { StateMachine } from 'aws-local-stepfunctions';
@@ -109,14 +119,15 @@ const machineDefinition = {
109119
},
110120
};
111121

112-
const stateMachine = new StateMachine(machineDefinition, { checkPaths: false });
122+
const stateMachine = new StateMachine(machineDefinition);
113123
const myInput = { value1: 'hello', value2: 123, value3: true };
114-
const result = await stateMachine.run(myInput); // execute the state machine
124+
const execution = stateMachine.run(myInput); // execute the state machine
115125

126+
const result = await execution.result; // wait until the execution finishes to get the result
116127
console.log(result); // log the result of the execution
117128
```
118129

119-
Example with `options`:
130+
##### Example with `options`:
120131

121132
```js
122133
import { StateMachine } from 'aws-local-stepfunctions';
@@ -146,9 +157,9 @@ function addNumbersLocal(input) {
146157
return input.num1 + input.num2;
147158
}
148159

149-
const stateMachine = new StateMachine(machineDefinition, { checkPaths: false });
160+
const stateMachine = new StateMachine(machineDefinition);
150161
const myInput = { value1: 'hello', value2: 123, value3: true };
151-
const result = await stateMachine.run(myInput, {
162+
const execution = stateMachine.run(myInput, {
152163
overrides: {
153164
taskResourceLocalHandlers: {
154165
AddNumbers: addNumbersLocal, // call the `addNumbersLocal` function instead of invoking the Lambda function specified for the `AddNumbers` state
@@ -159,7 +170,46 @@ const result = await stateMachine.run(myInput, {
159170
},
160171
});
161172

162-
console.log(result); // log the result of the execution
173+
const result = await execution.result;
174+
console.log(result);
175+
```
176+
177+
##### Aborting an execution
178+
179+
```js
180+
import { StateMachine, ExecutionAbortedError } from 'aws-local-stepfunctions';
181+
182+
const machineDefinition = {
183+
StartAt: 'Hello World',
184+
States: {
185+
'Hello World': {
186+
Type: 'Task',
187+
Resource: 'arn:aws:lambda:us-east-1:123456789012:function:HelloWorld',
188+
End: true,
189+
},
190+
},
191+
};
192+
193+
const stateMachine = new StateMachine(machineDefinition);
194+
const myInput = { value1: 'hello', value2: 123, value3: true };
195+
const execution = stateMachine.run(myInput);
196+
197+
// abort the execution after 3 seconds
198+
setTimeout(() => {
199+
execution.abort();
200+
}, 3000);
201+
202+
try {
203+
const result = await execution.result;
204+
console.log(result);
205+
} catch (e) {
206+
if (e instanceof ExecutionAbortedError) {
207+
// since execution was aborted, type of error is `ExecutionAbortedError`
208+
console.log('Execution was aborted');
209+
} else {
210+
console.error('Some other error', e);
211+
}
212+
}
163213
```
164214

165215
## License

__tests__/StateMachine.test.ts

+121-55
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,148 @@
11
import type { StateMachineDefinition } from '../src/typings/StateMachineDefinition';
22
import { StateMachine } from '../src/StateMachine';
3+
import { ExecutionAbortedError } from '../src/error/ExecutionAbortedError';
34

45
afterEach(() => {
56
jest.clearAllMocks();
67
});
78

89
describe('State Machine', () => {
9-
test('should validate ARNs by default when creating instance', async () => {
10-
const stateMachineDefinition: StateMachineDefinition = {
11-
StartAt: 'FirstState',
12-
States: {
13-
FirstState: {
14-
Type: 'Task',
15-
Resource: 'mock-arn',
16-
End: true,
10+
describe('constructor', () => {
11+
test('should validate ARNs by default when creating instance', async () => {
12+
const stateMachineDefinition: StateMachineDefinition = {
13+
StartAt: 'FirstState',
14+
States: {
15+
FirstState: {
16+
Type: 'Task',
17+
Resource: 'mock-arn',
18+
End: true,
19+
},
1720
},
18-
},
19-
};
21+
};
2022

21-
expect(() => {
22-
new StateMachine(stateMachineDefinition);
23-
}).toThrow();
24-
});
23+
expect(() => {
24+
new StateMachine(stateMachineDefinition);
25+
}).toThrow();
26+
});
2527

26-
test('should validate JSON paths by default when creating instance', async () => {
27-
const stateMachineDefinition: StateMachineDefinition = {
28-
StartAt: 'FirstState',
29-
States: {
30-
FirstState: {
31-
Type: 'Pass',
32-
InputPath: 'invalidPath',
33-
ResultPath: 'invalidPath',
34-
OutputPath: 'invalidPath',
35-
End: true,
28+
test('should validate JSON paths by default when creating instance', async () => {
29+
const stateMachineDefinition: StateMachineDefinition = {
30+
StartAt: 'FirstState',
31+
States: {
32+
FirstState: {
33+
Type: 'Pass',
34+
InputPath: 'invalidPath',
35+
ResultPath: 'invalidPath',
36+
OutputPath: 'invalidPath',
37+
End: true,
38+
},
3639
},
37-
},
38-
};
40+
};
3941

40-
expect(() => {
41-
new StateMachine(stateMachineDefinition);
42-
}).toThrow();
43-
});
42+
expect(() => {
43+
new StateMachine(stateMachineDefinition);
44+
}).toThrow();
45+
});
4446

45-
test('should not throw if ARN validation is turned off when creating instance', async () => {
46-
const stateMachineDefinition: StateMachineDefinition = {
47-
StartAt: 'FirstState',
48-
States: {
49-
FirstState: {
50-
Type: 'Task',
51-
Resource: 'mock-arn',
52-
End: true,
47+
test('should not throw if ARN validation is turned off when creating instance', async () => {
48+
const stateMachineDefinition: StateMachineDefinition = {
49+
StartAt: 'FirstState',
50+
States: {
51+
FirstState: {
52+
Type: 'Task',
53+
Resource: 'mock-arn',
54+
End: true,
55+
},
5356
},
54-
},
55-
};
56-
const validationOptions = { checkArn: false };
57+
};
58+
const validationOptions = { checkArn: false };
5759

58-
expect(() => {
59-
new StateMachine(stateMachineDefinition, validationOptions);
60-
}).not.toThrow();
60+
expect(() => {
61+
new StateMachine(stateMachineDefinition, validationOptions);
62+
}).not.toThrow();
63+
});
64+
65+
test('should not throw if JSON paths validation is turned off when creating instance', async () => {
66+
const stateMachineDefinition: StateMachineDefinition = {
67+
StartAt: 'FirstState',
68+
States: {
69+
FirstState: {
70+
Type: 'Pass',
71+
InputPath: 'invalidPath',
72+
ResultPath: 'invalidPath',
73+
OutputPath: 'invalidPath',
74+
End: true,
75+
},
76+
},
77+
};
78+
const validationOptions = { checkPaths: false };
79+
80+
expect(() => {
81+
new StateMachine(stateMachineDefinition, validationOptions);
82+
}).not.toThrow();
83+
});
6184
});
6285

63-
test('should not throw if JSON paths validation is turned off when creating instance', async () => {
64-
const stateMachineDefinition: StateMachineDefinition = {
65-
StartAt: 'FirstState',
86+
describe('run()', () => {
87+
const machineDefinition: StateMachineDefinition = {
88+
StartAt: 'PassState',
6689
States: {
67-
FirstState: {
90+
PassState: {
6891
Type: 'Pass',
69-
InputPath: 'invalidPath',
70-
ResultPath: 'invalidPath',
71-
OutputPath: 'invalidPath',
7292
End: true,
7393
},
7494
},
7595
};
76-
const validationOptions = { checkPaths: false };
7796

78-
expect(() => {
79-
new StateMachine(stateMachineDefinition, validationOptions);
80-
}).not.toThrow();
97+
test('should throw on abort if `noThrowOnAbort` option is not passed', async () => {
98+
const input = {};
99+
100+
const stateMachine = new StateMachine(machineDefinition);
101+
const execution = stateMachine.run(input);
102+
103+
execution.abort();
104+
105+
await expect(() => execution.result).rejects.toThrow(ExecutionAbortedError);
106+
});
107+
108+
test('should not throw on abort and return `null` if `noThrowOnAbort` option is passed', async () => {
109+
const input = {};
110+
111+
const stateMachine = new StateMachine(machineDefinition);
112+
const execution = stateMachine.run(input, { noThrowOnAbort: true });
113+
114+
execution.abort();
115+
116+
await expect(execution.result).resolves.toBe(null);
117+
});
118+
119+
test('should return result of last state as execution result', async () => {
120+
const machineDefinition: StateMachineDefinition = {
121+
StartAt: 'PassState1',
122+
States: {
123+
PassState1: {
124+
Type: 'Pass',
125+
Result: 1,
126+
Next: 'PassState2',
127+
},
128+
PassState2: {
129+
Type: 'Pass',
130+
Result: 2,
131+
Next: 'PassState3',
132+
},
133+
PassState3: {
134+
Type: 'Pass',
135+
Result: 3,
136+
End: true,
137+
},
138+
},
139+
};
140+
const input = {};
141+
142+
const stateMachine = new StateMachine(machineDefinition);
143+
const execution = stateMachine.run(input);
144+
145+
await expect(execution.result).resolves.toBe(3);
146+
});
81147
});
82148
});

__tests__/stateHandlers/WaitStateHandler.test.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('Wait State', () => {
2727
const waitStateHandler = new WaitStateHandler(definition);
2828
const { stateResult } = await waitStateHandler.executeState(input, context);
2929

30-
expect(mockSleepFunction).toHaveBeenCalledWith(10000);
30+
expect(mockSleepFunction).toHaveBeenCalledWith(10000, undefined);
3131
expect(stateResult).toEqual({ prop1: 'test', prop2: 12345 });
3232
});
3333

@@ -43,7 +43,7 @@ describe('Wait State', () => {
4343
const waitStateHandler = new WaitStateHandler(definition);
4444
const { stateResult } = await waitStateHandler.executeState(input, context);
4545

46-
expect(mockSleepFunction).toHaveBeenCalledWith(20700000);
46+
expect(mockSleepFunction).toHaveBeenCalledWith(20700000, undefined);
4747
expect(stateResult).toEqual({ prop1: 'test', prop2: 12345 });
4848
});
4949

@@ -59,7 +59,7 @@ describe('Wait State', () => {
5959
const waitStateHandler = new WaitStateHandler(definition);
6060
const { stateResult } = await waitStateHandler.executeState(input, context);
6161

62-
expect(mockSleepFunction).toHaveBeenCalledWith(10000);
62+
expect(mockSleepFunction).toHaveBeenCalledWith(10000, undefined);
6363
expect(stateResult).toEqual({ waitFor: 10 });
6464
});
6565

@@ -75,7 +75,7 @@ describe('Wait State', () => {
7575
const waitStateHandler = new WaitStateHandler(definition);
7676
const { stateResult } = await waitStateHandler.executeState(input, context);
7777

78-
expect(mockSleepFunction).toHaveBeenCalledWith(20700000);
78+
expect(mockSleepFunction).toHaveBeenCalledWith(20700000, undefined);
7979
expect(stateResult).toEqual({ waitUntil: '2022-12-05T05:45:00Z' });
8080
});
8181

@@ -87,13 +87,14 @@ describe('Wait State', () => {
8787
};
8888
const input = { waitUntil: '2022-12-05T05:45:00Z' };
8989
const context = {};
90-
const options = { waitTimeOverrideOption: 1500 };
90+
const abortSignal = new AbortController().signal;
91+
const options = { waitTimeOverrideOption: 1500, abortSignal };
9192

9293
const waitStateHandler = new WaitStateHandler(definition);
9394
const { stateResult } = await waitStateHandler.executeState(input, context, options);
9495

9596
expect(mockSleepFunction).toHaveBeenCalledTimes(1);
96-
expect(mockSleepFunction).toHaveBeenCalledWith(1500);
97+
expect(mockSleepFunction).toHaveBeenCalledWith(1500, abortSignal);
9798
expect(stateResult).toEqual({ waitUntil: '2022-12-05T05:45:00Z' });
9899
});
99100
});

0 commit comments

Comments
 (0)