Skip to content

Commit

Permalink
Add error handling to plugins on core's level (#142)
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

* chore(version control): init commit for wip pr

* refactor(package.json): added development packages and scripts (already in other open pr)

* chore(*): checkpoint

* feat(statecharts.ts): now subscribers are notified when a state plugin's action changes status

* chore(version control): removed build from verson control

* fix(*): removed workflow-browser-sdk from wrong branch
  • Loading branch information
Omri-Levy committed Feb 27, 2023
1 parent ae5be3e commit c018f88
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 74 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/tough-cameras-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ballerine/workflow-core': patch
---

workflow-core consumers may now listen to the status of state plugins (pending|idle)
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/dist
/tmp
/out-tsc
/build

# dependencies
/node_modules
Expand Down
7 changes: 2 additions & 5 deletions packages/workflow-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
// Type only exports - does not bundle otherwise.
export { createWorkflow, Error, HttpError } from './lib';
export type {
StatePlugin,
WorkflowEvent,
WorkflowRunnerArgs,
WorkflowEventWithoutState,
WorkflowOptions,
WorkflowRunnerArgs,
} from './lib';

export {
createWorkflow,
} from "./lib";
12 changes: 6 additions & 6 deletions packages/workflow-core/src/lib/create-workflow.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {TCreateWorkflow} from "./types";
import {WorkflowRunner} from "./workflow-runner";
import { TCreateWorkflow } from './types';
import { WorkflowRunner } from './workflow-runner';

export const createWorkflow: TCreateWorkflow = ({
workflowDefinition,
workflowActions,
extensions,
}) =>
workflowDefinition,
workflowActions,
extensions,
}) =>
new WorkflowRunner({
workflowDefinition,
workflowActions,
Expand Down
5 changes: 5 additions & 0 deletions packages/workflow-core/src/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class HttpError extends Error {
constructor(public status: number, message: string, cause?: unknown) {
super(message, {cause});
}
}
10 changes: 6 additions & 4 deletions packages/workflow-core/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export { createWorkflow } from './create-workflow';
export { HttpError } from './errors';
export {
Error,
StatePlugin,
WorkflowEvent,
WorkflowEventWithoutState,
WorkflowOptions,
WorkflowRunnerArgs,
WorkflowEventWithoutState,
StatePlugin,
} from "./types";
export {createWorkflow} from "./create-workflow";
} from './types';
13 changes: 7 additions & 6 deletions packages/workflow-core/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {MachineConfig, MachineOptions} from "xstate";
import type { MachineConfig, MachineOptions } from 'xstate';

export interface Workflow {
subscribe: (callback: (event: WorkflowEvent) => void) => void;
Expand All @@ -16,11 +16,7 @@ export interface WorkflowContext {

export interface WorkflowPlugin {
when: 'pre' | 'post';
action: (options: {
context: any;
event: any;
currentState: any;
}) => Promise<void>;
action: (options: { context: any; event: any; currentState: any }) => Promise<void>;
}

export interface StatePlugin extends Omit<WorkflowPlugin, 'when'> {
Expand Down Expand Up @@ -48,6 +44,7 @@ export interface WorkflowEvent {
type: string;
state: string;
payload?: Record<PropertyKey, any>;
error?: unknown;
}

export interface WorkflowExtensions {
Expand Down Expand Up @@ -79,3 +76,7 @@ export type GlobalPlugins = GlobalPlugin[];

export type TCreateWorkflow = (options: WorkflowOptions) => Workflow;

export const Error = {
ERROR: 'ERROR',
HTTP_ERROR: 'HTTP_ERROR',
} as const;
135 changes: 82 additions & 53 deletions packages/workflow-core/src/lib/workflow-runner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as jsonLogic from 'json-logic-js';
import {createMachine, interpret} from 'xstate';
import type {ActionFunction, MachineOptions, StateMachine, } from 'xstate';
import type { ActionFunction, MachineOptions, StateMachine } from 'xstate';
import { createMachine, interpret } from 'xstate';
import { HttpError } from './errors';
import type {
WorkflowEvent,
WorkflowEventWithoutState,
WorkflowExtensions,
WorkflowRunnerArgs
} from "./types";
WorkflowRunnerArgs,
} from './types';
import { Error } from './types';

export class WorkflowRunner {
#__subscription: Array<(event: WorkflowEvent) => void> = [];
Expand All @@ -27,42 +29,34 @@ export class WorkflowRunner {
}

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

// use initial context or provided context
this.#__context = Object.keys(context).length
? context
: workflowDefinition.context || {};
this.#__context = Object.keys(context).length ? context : workflowDefinition.context || {};

//use initial state or provided state
// use initial state or provided state
this.#__currentState = state ? state : workflowDefinition.initial;

// global and state specific extensions
this.#__extensions = extensions || {globalPlugins: [], statePlugins: []};
this.#__extensions = extensions || { globalPlugins: [], statePlugins: [] };
this.#__debugMode = debugMode;
}

#__extendedWorkflow({
workflow,
workflowActions,
extensions = {
statePlugins: [],
globalPlugins: [],
}
}: {
workflow,
workflowActions,
extensions = {
statePlugins: [],
globalPlugins: [],
},
}: {
workflow: any;
workflowActions?: WorkflowRunnerArgs['workflowActions'];
extensions?: WorkflowExtensions;
Expand All @@ -73,41 +67,79 @@ export class WorkflowRunner {
const stateActions: Record<string, ActionFunction<any, any>> = {};

for (const state in extended.states) {

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

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

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]),
);

// workflow-core
// { actions: { persist: action } }
stateActions[statePlugin.name] = async (context, event) => {
this.#__callback?.({
type: 'STATE_ACTION_STATUS',
state: this.#__currentState,
payload: {
status: 'PENDING',
},
});

try {
await statePlugin.action({
context,
event,
currentState: this.#__currentState,
});
} catch (err) {
let type;

switch (true) {
case err instanceof HttpError:
type = Error.HTTP_ERROR;
break;
default:
type = Error.ERROR;
break;
}

this.#__callback?.({
type,
state: this.#__currentState,
error: err,
});
} finally {
this.#__callback?.({
type: 'STATE_ACTION_STATUS',
state: this.#__currentState,
payload: {
status: 'IDLE',
},
});
}
};
}
}

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
]
));
new Set([...extended.states[stateName][statePlugin.when], statePlugin.name]),
);

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

}

const actions: MachineOptions<any, any>['actions'] = {
Expand All @@ -122,19 +154,16 @@ export class WorkflowRunner {
};

const guards: MachineOptions<any, any>['guards'] = {
'json-rule': (ctx, {payload}, {cond}) => {
const data = {...ctx, ...payload};
'json-rule': (ctx, { payload }, { cond }) => {
const data = { ...ctx, ...payload };
return jsonLogic.apply(
cond.name, // Rule
data // Data
data, // Data
);
},
};

return createMachine(
{predictableActionArguments: false, ...extended},
{actions, guards}
);
return createMachine({ predictableActionArguments: false, ...extended }, { actions, guards });
}

async sendEvent(event: WorkflowEventWithoutState) {
Expand All @@ -143,7 +172,7 @@ export class WorkflowRunner {

const service = interpret(workflow)
.start(this.#__currentState)
.onTransition((state) => {
.onTransition(state => {
if (state.changed) {
console.log('Transitioned into', state.value);

Expand Down Expand Up @@ -174,7 +203,7 @@ export class WorkflowRunner {
await ext.action({
context: service.getSnapshot().context,
event,
currentState: this.#__currentStateNode
currentState: this.#__currentStateNode,
});
}
}
Expand All @@ -189,7 +218,7 @@ export class WorkflowRunner {
await ext.action({
context: this.#__context,
event,
currentState: this.#__currentStateNode
currentState: this.#__currentStateNode,
});
}
}
Expand Down

0 comments on commit c018f88

Please sign in to comment.