Skip to content

Commit

Permalink
Inject entry/exit actions for state plugins (#141)
Browse files Browse the repository at this point in the history
* chore(version control): init commit for wip pr

* feat(package.json): added rimraf and a clean command for the build directory

* feat(package.json): now running pnpm clean before pnpm build

* fix(rollup.config.ts): removed the dts plugin from esm and cjs configs

otherwise there's no index.js in the output

* refactor(statecharts.ts): removed previous implementation of statePlugins

* fix(statecharts.ts): entry/exit actions injection now uses a set to avoid duplicates

* feat(statecharts.ts): added the ability to pass actions from outside into createMachine

now statePlugins are injected to a state's exit/entry as an array with unique values

* chore(changesets): added a changeset for latest changes

* ci(vite timestamp): removed vite.config.ts.timestamp file from remote
  • Loading branch information
Omri-Levy committed Feb 23, 2023
1 parent c2a0706 commit 3bb5b10
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-jokes-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ballerine/workflow-core': patch
---

fixed entry/exit plugins outputting duplicate actions
5 changes: 5 additions & 0 deletions .changeset/red-boats-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ballerine/workflow-core': patch
---

removed previous implementation of statePlugins
5 changes: 5 additions & 0 deletions .changeset/yellow-onions-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ballerine/workflow-core': patch
---

added state plugins, actions which runs on exit or entry of a state
10 changes: 7 additions & 3 deletions packages/workflow-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
"license": "Apache-2.0",
"keywords": [],
"scripts": {
"bundle": "rollup --config rollup.config.js",
"build:main": "tsc -p tsconf.dev.json",
"clean": "rimraf ./build",
"build": "pnpm clean && rollup --config rollup.config.js",
"watch": "tsc -w",
"dev": "concurrently --kill-others \"pnpm build -w\" \"pnpm watch\"",
"test": "vitest run"
},
"engines": {
Expand All @@ -45,6 +47,7 @@
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"@vitest/coverage-istanbul": "^0.28.4",
"concurrently": "^7.6.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.32.0",
"eslint-config-prettier": "^6.11.0",
Expand All @@ -55,6 +58,7 @@
"plugin-babel": "link:@types/@rollup/plugin-babel",
"plugin-terser": "link:@types/@rollup/plugin-terser",
"prettier": "^2.1.1",
"rimraf": "^4.1.2",
"rollup": "2.70.2",
"rollup-plugin-dts": "4.2.2",
"rollup-plugin-size": "0.2.2",
Expand All @@ -64,4 +68,4 @@
"vite": "^4.1.1",
"vitest": "^0.28.4"
}
}
}
5 changes: 2 additions & 3 deletions packages/workflow-core/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function esm({ input, packageDir, external, banner }: Options): RollupOptions {
banner,
preserveModules: true,
},
plugins: [babelPlugin, nodeResolve({ extensions: ['.ts'] }), dts()],
plugins: [babelPlugin, nodeResolve({ extensions: ['.ts'] })],
};
}

Expand All @@ -117,7 +117,7 @@ function cjs({ input, external, packageDir, banner }: Options): RollupOptions {
exports: 'named',
banner,
},
plugins: [babelPlugin, commonjs(), nodeResolve({ extensions: ['.ts'] }),dts()],
plugins: [babelPlugin, commonjs(), nodeResolve({ extensions: ['.ts'] })],
};
}

Expand All @@ -144,7 +144,6 @@ function umdDev({
commonjs(),
nodeResolve({ extensions: ['.ts'] }),
umdDevPlugin('development'),
dts()
],
};
}
Expand Down
20 changes: 18 additions & 2 deletions packages/workflow-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MachineConfig } from 'xstate';
import {MachineConfig, MachineOptions} from 'xstate';
import { WorkflowRunner } from './lib/statecharts';

export * from './lib/statecharts';
Expand Down Expand Up @@ -26,7 +26,20 @@ interface WorkflowPlugin {
}) => Promise<void>;
}

interface StatePlugin extends WorkflowPlugin {
interface StatePlugin extends Omit<WorkflowPlugin, 'when'> {
/**
* The actions key to inject an action function into.
* E.g. { actions: { [plugin.name]: plugin.action } }
*/
name: string;
/**
* entry - fire an action when transitioning into a specified state
* exit - fire an action when transitioning out of a specified state
*/
when: 'entry' | 'exit';
/**
* States already defined in the statechart
*/
stateNames: Array<string>;
}

Expand All @@ -53,6 +66,7 @@ export interface WorkflowExtensions {
export interface WorkflowOptions {
workflowDefinitionType: 'statechart-json' | 'bpmn-json';
workflowDefinition: MachineConfig<any, any, any>;
workflowActions?: MachineOptions<any, any>['actions'];
context?: WorkflowContext;
extensions?: WorkflowExtensions;
}
Expand All @@ -61,10 +75,12 @@ type TCreateWorkflow = (options: WorkflowOptions) => Workflow;

export const createWorkflow: TCreateWorkflow = ({
workflowDefinition,
workflowActions,
extensions,
}) =>
new WorkflowRunner({
workflowDefinition,
workflowActions,
context: {},
extensions,
});
6 changes: 4 additions & 2 deletions packages/workflow-core/src/lib/statecharts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ test('Basic workflow sanity test', async () => {
extensions: {
statePlugins: [
{
name: 'activeAction',
stateNames: ['active'],
when: 'pre',
when: 'exit',
// import: '@ballerine/plugins/core/validate@0.2.34',
// import: '@ballerine/plugins/browser/validate@0.2.34',
// import: '@ballerine/plugins/node/validate@0.2.34',
Expand All @@ -48,8 +49,9 @@ test('Basic workflow sanity test', async () => {
},
},
{
name: 'inactiveAction',
stateNames: ['inactive'],
when: 'post',
when: 'exit',
action: ({context, event, currentState}) => {
console.log('state post action');
return Promise.resolve();
Expand Down
112 changes: 73 additions & 39 deletions packages/workflow-core/src/lib/statecharts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import {createMachine, interpret, MachineConfig, StateMachine} from 'xstate';
import {
ActionFunction,
createMachine,
interpret,
MachineConfig,
MachineOptions,
StateMachine
} from 'xstate';
import * as jsonLogic from 'json-logic-js';

import {
Expand All @@ -9,6 +16,7 @@ import {

interface WorkflowRunnerArgs {
workflowDefinition: MachineConfig<any, any, any>;
workflowActions?: MachineOptions<any, any>['actions'];
context: any;
state?: string;
extensions?: WorkflowExtensions;
Expand All @@ -33,10 +41,20 @@ export class WorkflowRunner {
}

constructor(
{workflowDefinition, context = {}, state, extensions}: WorkflowRunnerArgs,
{
workflowDefinition,
workflowActions,
context = {},
state,
extensions
}: WorkflowRunnerArgs,
debugMode = true
) {
this.#__workflow = this.#__extendedWorkflow(workflowDefinition);
this.#__workflow = this.#__extendedWorkflow({
workflow: workflowDefinition,
workflowActions,
extensions
});

// use initial context or provided context
this.#__context = Object.keys(context).length
Expand All @@ -51,32 +69,74 @@ export class WorkflowRunner {
this.#__debugMode = debugMode;
}

#__extendedWorkflow(workflow: any) {
#__extendedWorkflow({
workflow,
workflowActions,
extensions = {
statePlugins: [],
globalPlugins: [],
}
}: {
workflow: any;
workflowActions?: WorkflowRunnerArgs['workflowActions'];
extensions?: WorkflowExtensions;
}) {
const extended = workflow;
const onEnter = ['ping'];
const onExit = ['pong'];
const stateActions: Record<string, ActionFunction<any, any>> = {};

for (const state in extended.states) {
extended.states[state].entry = onEnter.concat(
workflow.states[state].entry || [],
onEnter

extended.states[state].entry = Array.from(
new Set([
...(workflow.states[state].entry ?? []),
...onEnter
])
);
extended.states[state].exit = onExit.concat(
workflow.states[state].exit || [],
onExit

extended.states[state].exit = Array.from(
new Set([
...(workflow.states[state].exit ?? []),
...onExit
])
);


}

const actions = {
for (const statePlugin of extensions.statePlugins) {

for (const stateName of statePlugin.stateNames) {

// E.g { state: { entry: [...,plugin.name] } }
extended.states[stateName][statePlugin.when] = Array.from(
new Set(
[
...extended.states[stateName][statePlugin.when],
statePlugin.name
]
));

// { actions: { persist: action } }
stateActions[statePlugin.name] = statePlugin.action;
}

}

const actions: MachineOptions<any, any>['actions'] = {
...workflowActions,
...stateActions,
ping: (...rest: any[]) => {
console.log('Global state entry handler');
},
pong: (...rest: any[]) => {
console.log('Global state exit handler');
},
};
const guards = {
'json-rule': (ctx: any, {payload}: any, {cond}: any) => {

const guards: MachineOptions<any, any>['guards'] = {
'json-rule': (ctx, {payload}, {cond}) => {
const data = {...ctx, ...payload};
return jsonLogic.apply(
cond.name, // Rule
Expand Down Expand Up @@ -123,19 +183,6 @@ export class WorkflowRunner {
// all sends() will be deferred until the workflow is started
service.start();

for (const ext of this.#__extensions.statePlugins) {
if (
ext.when !== 'pre' ||
!ext.stateNames?.includes(this.#__currentState)
) continue;

await ext.action({
context: service.getSnapshot().context,
event,
currentState: this.#__currentStateNode
});
}

for (const ext of this.#__extensions.globalPlugins) {
if (ext.when == 'pre') {
await ext.action({
Expand All @@ -151,19 +198,6 @@ export class WorkflowRunner {
console.log('context:', this.#__context);
}

for (const ext of this.#__extensions.statePlugins) {
if (
ext.when !== 'post' ||
!ext.stateNames?.includes(this.#__currentState)
) continue;

await ext.action({
context: service.getSnapshot().context,
event,
currentState: this.#__currentStateNode
});
}

for (const ext of this.#__extensions.globalPlugins) {
if (ext.when == 'post') {
await ext.action({
Expand Down
Loading

0 comments on commit 3bb5b10

Please sign in to comment.