Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/stepfunctions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name: Stepfunctions
on:
push:
branches: [master]
# Run on every pull request, including ones stacked on feature branches.
pull_request:
branches: [master]

jobs:
build:
Expand All @@ -22,6 +22,7 @@ jobs:
- run: npm install
- run: npm run lint
- run: npm test
- run: npm run test:smoke
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ node_modules
.env
*.log
*.lock
*.tgz
.serverless
coverage
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,29 @@ If you are:

## Usage

Include it in your test files, tested with Jest so far.
The package ships both ESM and CommonJS entry points with TypeScript types, so use whichever you prefer:

```js
// ESM
import Sfn from 'stepfunctions';
// or: import { StepFunction } from 'stepfunctions';

// CommonJS
const Sfn = require('stepfunctions');
```

Include it in your test files (tested with Jest so far). You can pass a bare Amazon States Language definition straight to the constructor, and `startExecution` resolves to the final output:

```js
const Sfn = require('stepfunctions');

const sm = new Sfn({
StateMachine: {
StartAt: 'Test',
States: {
Test: {
Type: 'Task',
Resource: 'arn:aws:lambda:ap-southeast-1:123456789012:function:test',
End: true,
},
StartAt: 'Test',
States: {
Test: {
Type: 'Task',
Resource: 'arn:aws:lambda:ap-southeast-1:123456789012:function:test',
End: true,
},
},
});
Expand All @@ -56,20 +65,25 @@ describe('StateMachine Test', () => {
it('Check if a task was run', async () => {
const mockfn = jest.fn((input) => input.test === 1);
sm.bindTaskResource('Test', mockfn);
await sm.startExecution({ test: 1 });
const result = await sm.startExecution({ test: 1 });
expect(mockfn).toHaveBeenCalled();
expect(result).toBe(true);
});
});
```

The `new Sfn({ StateMachine })` form is still supported for backwards compatibility, as is calling `getExecutionResult()` after `startExecution` (it is now idempotent — it no longer consumes the result).

You can see more examples in `/test/stepfunctions.test.js`.

## API

### startExecution(Input, Options);

Resolves to the final output of the execution.

```js
sm.startExecution(input, {
const output = await sm.startExecution(input, {
respectTime: false,
maxWaitTime: 30,
maxConcurrency: 10,
Expand Down Expand Up @@ -105,7 +119,7 @@ Must be called before `startExecution`, binds to `Tasks` and replaces their hand

### getExecutionResult()

Must be called after `startExecution`. This function returns the absolute result from the statemachine if it has finished.
Must be called after `startExecution`. This function returns the absolute result from the statemachine if it has finished. It is idempotent — repeated calls return the same value. (`startExecution` also resolves to this value, so you often don't need it.)

### getReport()

Expand Down
6 changes: 6 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ module.exports = [
'no-unused-vars': ['error', { args: 'none', varsIgnorePattern: '^_' }],
},
},
{
files: ['**/*.mjs'],
languageOptions: {
sourceType: 'module',
},
},
];
50 changes: 50 additions & 0 deletions index.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { EventEmitter } from 'events';

/** Options accepted by `startExecution`. */
export interface ExecutionOptions {
/** Respect the real durations of `Wait` states instead of capping them. */
respectTime?: boolean;
/** Maximum number of seconds a `Wait` state may block for. Defaults to 30. */
maxWaitTime?: number;
/** Maximum number of branches/iterations run concurrently. Defaults to 10. */
maxConcurrency?: number;
}

/** A bound Task handler, e.g. a Serverless/Lambda handler. */
export type TaskResource = (input: any) => any;

/** An Amazon States Language state machine definition. */
export type StateMachine = Record<string, any>;

export interface Options {
/** Friendly name for the execution. Defaults to the StateMachine `StartAt`. */
Name?: string;
StateMachine: StateMachine;
/** Map of Task state name to its handler. */
Resources?: Record<string, TaskResource>;
}

/**
* An in-memory AWS Step Functions / Amazon States Language interpreter, made to
* run Node.js Lambda handlers inside a test environment.
*/
export declare class StepFunction extends EventEmitter {
constructor(sm: Options | StateMachine);

/**
* Start an execution, passing `input` to the first state. Resolves to the
* final output (the same value as `getExecutionResult()`).
*/
startExecution(input?: any, opts?: ExecutionOptions): Promise<any>;

/** Return the final output of the most recent execution. */
getExecutionResult(): any;

/** Pretty-print the recorded transitions with `console.table`. */
getReport(): void;

/** Replace a Task's resource with the provided handler. */
bindTaskResource(task: string, fn: TaskResource): void;
}

export default StepFunction;
9 changes: 6 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ declare namespace StepFunction {
* run Node.js Lambda handlers inside a test environment.
*/
declare class StepFunction extends EventEmitter {
constructor(sm: StepFunction.Options);
constructor(sm: StepFunction.Options | StepFunction.StateMachine);

/** Start an execution, passing `input` to the first state. */
/**
* Start an execution, passing `input` to the first state. Resolves to the
* final output (the same value as `getExecutionResult()`).
*/
startExecution(
input?: any,
opts?: StepFunction.ExecutionOptions,
): Promise<void>;
): Promise<any>;

/** Return the final output of the most recent execution. */
getExecutionResult(): any;
Expand Down
4 changes: 4 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import StepFunction from './lib/stepfunctions.js';

export default StepFunction;
export { StepFunction };
20 changes: 14 additions & 6 deletions lib/stepfunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,19 @@ class StepFunction extends EventEmitter {
*/
constructor(sm) {
super();
const { isValid, errorsText } = aslValidator(sm.StateMachine);
// Accept either `{ StateMachine, Name?, Resources? }` or a bare Amazon
// States Language definition (`{ StartAt, States, ... }`) directly.
const opts = sm && sm.StateMachine ? sm : { StateMachine: sm };
const { isValid, errorsText } = aslValidator(opts.StateMachine);
if (!isValid) {
throw new Error(errorsText());
}

this.Name = sm.Name || sm.StateMachine.StartAt;
this.StateMachine = sm.StateMachine;
this.Resources = typeof sm.Resources === 'object' ? sm.Resources : {};
this.Name = opts.Name || opts.StateMachine.StartAt;
this.StateMachine = opts.StateMachine;
this.Resources = typeof opts.Resources === 'object' ? opts.Resources : {};
// Default query language for the whole machine; a state may override it.
this.queryLanguage = sm.StateMachine.QueryLanguage || 'JSONPath';
this.queryLanguage = opts.StateMachine.QueryLanguage || 'JSONPath';
// Each Map iteration / Parallel branch runs in its own variable scope; an
// AsyncLocalStorage keeps those scopes isolated even under concurrency.
this.variableStore = new AsyncLocalStorage();
Expand Down Expand Up @@ -116,6 +119,9 @@ class StepFunction extends EventEmitter {
throw err;
}
}
// Return the result directly so callers can `const out = await
// sm.startExecution(...)` without a separate getExecutionResult() call.
return this.getExecutionResult();
}

/**
Expand All @@ -127,7 +133,9 @@ class StepFunction extends EventEmitter {
* @returns {any}
*/
getExecutionResult() {
return this.steps.pop().output;
// Non-mutating: returns the final output and can be called repeatedly.
const last = this.steps[this.steps.length - 1];
return last ? last.output : undefined;
}

/**
Expand Down
51 changes: 3 additions & 48 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 23 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@
"description": "AWS Step Functions implementation in Node, so you can run your Node.js lambda handlers in your test environments. Made to support Serverless JS testing.",
"version": "0.2.0",
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"exports": {
".": {
"import": {
"types": "./index.d.mts",
"default": "./index.mjs"
},
"require": {
"types": "./index.d.ts",
"default": "./index.js"
}
},
"./package.json": "./package.json"
},
"license": "MIT",
"repository": {
"type": "git",
Expand All @@ -16,12 +30,15 @@
"lib",
"LICENSE",
"index.js",
"index.mjs",
"index.d.ts",
"index.d.mts",
"package.json"
],
"scripts": {
"test": "jest",
"lint": "eslint lib test index.js && prettier --check .",
"test:smoke": "node test/smoke.mjs",
"lint": "eslint lib test index.js index.mjs && prettier --check .",
"format": "prettier --write .",
"prepare": "husky || true"
},
Expand All @@ -39,6 +56,10 @@
"lint-staged": "^15.2.10",
"prettier": "^3.3.3"
},
"overrides": {
"underscore": "^1.13.8",
"js-yaml": "^4.2.0"
},
"peerDependencies": {
"serverless": ">=1.74.1"
},
Expand All @@ -48,7 +69,7 @@
}
},
"lint-staged": {
"*.js": [
"*.{js,mjs}": [
"eslint --fix",
"prettier --write"
]
Expand Down
Loading
Loading