From c018f88e4ff4dddad7887d8a72872a6643bd56b3 Mon Sep 17 00:00:00 2001 From: Omri Levy <61207713+Omri-Levy@users.noreply.github.com> Date: Mon, 27 Feb 2023 17:08:53 +0200 Subject: [PATCH] Add error handling to plugins on core's level (#142) * 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 --- .changeset/afraid-jokes-matter.md | 5 + .changeset/red-boats-do.md | 5 + .changeset/tough-cameras-collect.md | 5 + .changeset/yellow-onions-cross.md | 5 + .gitignore | 1 + packages/workflow-core/src/index.ts | 7 +- .../workflow-core/src/lib/create-workflow.ts | 12 +- packages/workflow-core/src/lib/errors.ts | 5 + packages/workflow-core/src/lib/index.ts | 10 +- packages/workflow-core/src/lib/types.ts | 13 +- .../workflow-core/src/lib/workflow-runner.ts | 135 +++++++++++------- 11 files changed, 129 insertions(+), 74 deletions(-) create mode 100644 .changeset/afraid-jokes-matter.md create mode 100644 .changeset/red-boats-do.md create mode 100644 .changeset/tough-cameras-collect.md create mode 100644 .changeset/yellow-onions-cross.md create mode 100644 packages/workflow-core/src/lib/errors.ts diff --git a/.changeset/afraid-jokes-matter.md b/.changeset/afraid-jokes-matter.md new file mode 100644 index 0000000000..fa1629190c --- /dev/null +++ b/.changeset/afraid-jokes-matter.md @@ -0,0 +1,5 @@ +--- +'@ballerine/workflow-core': patch +--- + +fixed entry/exit plugins outputting duplicate actions diff --git a/.changeset/red-boats-do.md b/.changeset/red-boats-do.md new file mode 100644 index 0000000000..c71c1ee115 --- /dev/null +++ b/.changeset/red-boats-do.md @@ -0,0 +1,5 @@ +--- +'@ballerine/workflow-core': patch +--- + +removed previous implementation of statePlugins diff --git a/.changeset/tough-cameras-collect.md b/.changeset/tough-cameras-collect.md new file mode 100644 index 0000000000..2ea4907adb --- /dev/null +++ b/.changeset/tough-cameras-collect.md @@ -0,0 +1,5 @@ +--- +'@ballerine/workflow-core': patch +--- + +workflow-core consumers may now listen to the status of state plugins (pending|idle) diff --git a/.changeset/yellow-onions-cross.md b/.changeset/yellow-onions-cross.md new file mode 100644 index 0000000000..2b04ffd2cb --- /dev/null +++ b/.changeset/yellow-onions-cross.md @@ -0,0 +1,5 @@ +--- +'@ballerine/workflow-core': patch +--- + +added state plugins, actions which runs on exit or entry of a state diff --git a/.gitignore b/.gitignore index 09b0078c75..d5dc70acd0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /dist /tmp /out-tsc +/build # dependencies /node_modules diff --git a/packages/workflow-core/src/index.ts b/packages/workflow-core/src/index.ts index 20623fcee9..bcd9e511bd 100644 --- a/packages/workflow-core/src/index.ts +++ b/packages/workflow-core/src/index.ts @@ -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"; diff --git a/packages/workflow-core/src/lib/create-workflow.ts b/packages/workflow-core/src/lib/create-workflow.ts index adc786f72e..a0634b5b98 100644 --- a/packages/workflow-core/src/lib/create-workflow.ts +++ b/packages/workflow-core/src/lib/create-workflow.ts @@ -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, diff --git a/packages/workflow-core/src/lib/errors.ts b/packages/workflow-core/src/lib/errors.ts new file mode 100644 index 0000000000..1b685f5e0f --- /dev/null +++ b/packages/workflow-core/src/lib/errors.ts @@ -0,0 +1,5 @@ +export class HttpError extends Error { + constructor(public status: number, message: string, cause?: unknown) { + super(message, {cause}); + } +} diff --git a/packages/workflow-core/src/lib/index.ts b/packages/workflow-core/src/lib/index.ts index a8f47355b3..f3a08e4721 100644 --- a/packages/workflow-core/src/lib/index.ts +++ b/packages/workflow-core/src/lib/index.ts @@ -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'; diff --git a/packages/workflow-core/src/lib/types.ts b/packages/workflow-core/src/lib/types.ts index 6e2d6708cc..a7e1c4489b 100644 --- a/packages/workflow-core/src/lib/types.ts +++ b/packages/workflow-core/src/lib/types.ts @@ -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; @@ -16,11 +16,7 @@ export interface WorkflowContext { export interface WorkflowPlugin { when: 'pre' | 'post'; - action: (options: { - context: any; - event: any; - currentState: any; - }) => Promise; + action: (options: { context: any; event: any; currentState: any }) => Promise; } export interface StatePlugin extends Omit { @@ -48,6 +44,7 @@ export interface WorkflowEvent { type: string; state: string; payload?: Record; + error?: unknown; } export interface WorkflowExtensions { @@ -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; diff --git a/packages/workflow-core/src/lib/workflow-runner.ts b/packages/workflow-core/src/lib/workflow-runner.ts index 7b352fdac2..979b121b4f 100644 --- a/packages/workflow-core/src/lib/workflow-runner.ts +++ b/packages/workflow-core/src/lib/workflow-runner.ts @@ -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> = []; @@ -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; @@ -73,41 +67,79 @@ export class WorkflowRunner { const stateActions: Record> = {}; 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['actions'] = { @@ -122,19 +154,16 @@ export class WorkflowRunner { }; const guards: MachineOptions['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) { @@ -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); @@ -174,7 +203,7 @@ export class WorkflowRunner { await ext.action({ context: service.getSnapshot().context, event, - currentState: this.#__currentStateNode + currentState: this.#__currentStateNode, }); } } @@ -189,7 +218,7 @@ export class WorkflowRunner { await ext.action({ context: this.#__context, event, - currentState: this.#__currentStateNode + currentState: this.#__currentStateNode, }); } }