From dc9e1b6f9e6e75a9876ffe67382ff6c6a0038115 Mon Sep 17 00:00:00 2001 From: b4rtaz Date: Sat, 10 Aug 2024 17:27:07 +0200 Subject: [PATCH] 0.7.0. --- CHANGELOG.md | 4 + machine/package.json | 6 +- machine/src/activities/index.ts | 1 + .../src/activities/signal-activity/index.ts | 2 + .../signal-activity-node-builder.ts | 85 +++++++++++++++++++ .../signal-activity/signal-activity.spec.ts | 81 ++++++++++++++++++ .../signal-activity/signal-activity.ts | 22 +++++ .../src/activities/signal-activity/types.ts | 24 ++++++ machine/src/machine-unhandled-error.ts | 6 +- machine/src/types.ts | 2 + machine/src/workflow-machine-interpreter.ts | 8 +- machine/src/workflow-machine.ts | 5 +- yarn.lock | 8 +- 13 files changed, 242 insertions(+), 12 deletions(-) create mode 100755 machine/src/activities/signal-activity/index.ts create mode 100755 machine/src/activities/signal-activity/signal-activity-node-builder.ts create mode 100755 machine/src/activities/signal-activity/signal-activity.spec.ts create mode 100755 machine/src/activities/signal-activity/signal-activity.ts create mode 100755 machine/src/activities/signal-activity/types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 38fbc24..05df1ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.0 + +This version introduces the signal activity. The signal activity stops the execution of the workflow machine and waits for a signal. + ## 0.6.0 This version introduces the [parallel activity](https://nocode-js.com/docs/sequential-workflow-machine/activities/parallel-activity). The parallel activity allows to execute in the same time many activities. diff --git a/machine/package.json b/machine/package.json index 4d54f0d..4befeaa 100644 --- a/machine/package.json +++ b/machine/package.json @@ -1,7 +1,7 @@ { "name": "sequential-workflow-machine", "description": "Powerful sequential workflow machine for frontend and backend applications.", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "./lib/esm/index.js", "types": "./lib/index.d.ts", @@ -58,7 +58,7 @@ "jest": "^29.4.3", "ts-jest": "^29.0.5", "typescript": "^4.9.5", - "prettier": "^2.8.4", + "prettier": "^3.3.3", "rollup": "^3.18.0", "rollup-plugin-dts": "^5.2.0", "rollup-plugin-typescript2": "^0.34.1", @@ -72,4 +72,4 @@ "nocode", "lowcode" ] -} \ No newline at end of file +} diff --git a/machine/src/activities/index.ts b/machine/src/activities/index.ts index e1113de..3d4eaa1 100644 --- a/machine/src/activities/index.ts +++ b/machine/src/activities/index.ts @@ -6,3 +6,4 @@ export * from './interruption-activity'; export * from './loop-activity'; export * from './parallel-activity'; export * from './results'; +export * from './signal-activity'; diff --git a/machine/src/activities/signal-activity/index.ts b/machine/src/activities/signal-activity/index.ts new file mode 100755 index 0000000..51949e7 --- /dev/null +++ b/machine/src/activities/signal-activity/index.ts @@ -0,0 +1,2 @@ +export * from './signal-activity'; +export * from './types'; diff --git a/machine/src/activities/signal-activity/signal-activity-node-builder.ts b/machine/src/activities/signal-activity/signal-activity-node-builder.ts new file mode 100755 index 0000000..2a27f17 --- /dev/null +++ b/machine/src/activities/signal-activity/signal-activity-node-builder.ts @@ -0,0 +1,85 @@ +import { SignalActivityConfig } from './types'; +import { EventObject } from 'xstate'; +import { Step } from 'sequential-workflow-model'; +import { ActivityStateProvider, catchUnhandledError, getStepNodeId } from '../../core'; +import { + ActivityNodeBuilder, + ActivityNodeConfig, + MachineContext, + SignalPayload, + STATE_FAILED_TARGET, + STATE_INTERRUPTED_TARGET +} from '../../types'; +import { isInterruptResult } from '../results'; + +export class SignalActivityNodeBuilder + implements ActivityNodeBuilder +{ + public constructor(private readonly config: SignalActivityConfig) {} + + public build(step: TStep, nextNodeTarget: string): ActivityNodeConfig { + const activityStateProvider = new ActivityStateProvider(step, this.config.init); + const nodeId = getStepNodeId(step.id); + + return { + id: nodeId, + initial: 'BEFORE_SIGNAL', + states: { + BEFORE_SIGNAL: { + invoke: { + src: catchUnhandledError(async (context: MachineContext) => { + const activityState = activityStateProvider.get(context, nodeId); + + const result = await this.config.beforeSignal(step, context.globalState, activityState); + if (isInterruptResult(result)) { + context.interrupted = nodeId; + return; + } + }), + onDone: [ + { + target: STATE_INTERRUPTED_TARGET, + cond: (context: MachineContext) => Boolean(context.interrupted) + }, + { + target: 'WAIT_FOR_SIGNAL' + } + ], + onError: STATE_FAILED_TARGET + } + }, + WAIT_FOR_SIGNAL: { + on: { + SIGNAL_RECEIVED: { + target: 'AFTER_SIGNAL' + } + } + }, + AFTER_SIGNAL: { + invoke: { + src: catchUnhandledError(async (context: MachineContext, event: EventObject) => { + const activityState = activityStateProvider.get(context, nodeId); + const ev = event as { type: string; payload: SignalPayload }; + + const result = await this.config.afterSignal(step, context.globalState, activityState, ev.payload); + if (isInterruptResult(result)) { + context.interrupted = nodeId; + return; + } + }), + onDone: [ + { + target: STATE_INTERRUPTED_TARGET, + cond: (context: MachineContext) => Boolean(context.interrupted) + }, + { + target: nextNodeTarget + } + ], + onError: STATE_FAILED_TARGET + } + } + } + }; + } +} diff --git a/machine/src/activities/signal-activity/signal-activity.spec.ts b/machine/src/activities/signal-activity/signal-activity.spec.ts new file mode 100755 index 0000000..9d8c473 --- /dev/null +++ b/machine/src/activities/signal-activity/signal-activity.spec.ts @@ -0,0 +1,81 @@ +import { Definition, Step } from 'sequential-workflow-model'; +import { createSignalActivity, signalSignalActivity } from './signal-activity'; +import { createActivitySet } from '../../core'; +import { createWorkflowMachineBuilder } from '../../workflow-machine-builder'; +import { STATE_FINISHED_ID } from '../../types'; + +interface TestGlobalState { + beforeCalled: boolean; + afterCalled: boolean; +} + +const activitySet = createActivitySet([ + createSignalActivity('waitForSignal', { + init: () => ({}), + beforeSignal: async (_, globalState) => { + expect(globalState.beforeCalled).toBe(false); + expect(globalState.afterCalled).toBe(false); + globalState.beforeCalled = true; + }, + afterSignal: async (_, globalState, __, payload) => { + expect(globalState.beforeCalled).toBe(true); + expect(globalState.afterCalled).toBe(false); + globalState.afterCalled = true; + expect(payload['TEST_VALUE']).toBe(123456); + expect(Object.keys(payload).length).toBe(1); + } + }) +]); + +describe('SignalActivity', () => { + it('stops, after signal continues', done => { + const definition: Definition = { + sequence: [ + { + id: '0x1', + componentType: 'task', + type: 'waitForSignal', + name: 'W8', + properties: {} + } + ], + properties: {} + }; + + const builder = createWorkflowMachineBuilder(activitySet); + const machine = builder.build(definition); + const interpreter = machine.create({ + init: () => ({ + afterCalled: false, + beforeCalled: false + }) + }); + + interpreter.onChange(() => { + const snapshot = interpreter.getSnapshot(); + + if (snapshot.tryGetStatePath()?.includes('WAIT_FOR_SIGNAL')) { + expect(snapshot.globalState.beforeCalled).toBe(true); + expect(snapshot.globalState.afterCalled).toBe(false); + + setTimeout(() => { + signalSignalActivity(interpreter, { + TEST_VALUE: 123456 + }); + }, 25); + } + }); + + interpreter.onDone(() => { + const snapshot = interpreter.getSnapshot(); + + expect(snapshot.tryGetStatePath()).toStrictEqual([STATE_FINISHED_ID]); + expect(snapshot.globalState.beforeCalled).toBe(true); + expect(snapshot.globalState.afterCalled).toBe(true); + + done(); + }); + + interpreter.start(); + }); +}); diff --git a/machine/src/activities/signal-activity/signal-activity.ts b/machine/src/activities/signal-activity/signal-activity.ts new file mode 100755 index 0000000..ff6925c --- /dev/null +++ b/machine/src/activities/signal-activity/signal-activity.ts @@ -0,0 +1,22 @@ +import { Activity, SignalPayload } from '../../types'; +import { WorkflowMachineInterpreter } from '../../workflow-machine-interpreter'; +import { SignalActivityNodeBuilder } from './signal-activity-node-builder'; +import { SignalActivityConfig } from './types'; +import { Step } from 'sequential-workflow-model'; + +export function createSignalActivity( + stepType: TStep['type'], + config: SignalActivityConfig +): Activity { + return { + stepType, + nodeBuilderFactory: () => new SignalActivityNodeBuilder(config) + }; +} + +export function signalSignalActivity( + interpreter: WorkflowMachineInterpreter, + payload: P +) { + interpreter.sendSignal('SIGNAL_RECEIVED', payload); +} diff --git a/machine/src/activities/signal-activity/types.ts b/machine/src/activities/signal-activity/types.ts new file mode 100755 index 0000000..790de01 --- /dev/null +++ b/machine/src/activities/signal-activity/types.ts @@ -0,0 +1,24 @@ +import { Step } from 'sequential-workflow-model'; +import { ActivityStateInitializer, SignalPayload } from '../../types'; +import { InterruptResult } from '../results'; + +export type BeforeSignalActivityHandler = ( + step: TStep, + globalState: GlobalState, + activityState: ActivityState +) => Promise; + +export type AfterSignalActivityHandler = ( + step: TStep, + globalState: GlobalState, + activityState: ActivityState, + signalPayload: SignalPayload +) => Promise; + +export type SignalActivityHandlerResult = void | InterruptResult; + +export interface SignalActivityConfig { + init: ActivityStateInitializer; + beforeSignal: BeforeSignalActivityHandler; + afterSignal: AfterSignalActivityHandler; +} diff --git a/machine/src/machine-unhandled-error.ts b/machine/src/machine-unhandled-error.ts index 693dc78..ed2ab53 100644 --- a/machine/src/machine-unhandled-error.ts +++ b/machine/src/machine-unhandled-error.ts @@ -1,5 +1,9 @@ export class MachineUnhandledError extends Error { - public constructor(message: string, public readonly cause: unknown, public readonly stepId: string | null) { + public constructor( + message: string, + public readonly cause: unknown, + public readonly stepId: string | null + ) { super(message); } } diff --git a/machine/src/types.ts b/machine/src/types.ts index 5428c78..d6109b1 100644 --- a/machine/src/types.ts +++ b/machine/src/types.ts @@ -55,3 +55,5 @@ export type SequentialStateMachineInterpreter = Interpreter< // eslint-disable-next-line @typescript-eslint/no-explicit-any any >; + +export type SignalPayload = Record; diff --git a/machine/src/workflow-machine-interpreter.ts b/machine/src/workflow-machine-interpreter.ts index 722f224..62b37d2 100644 --- a/machine/src/workflow-machine-interpreter.ts +++ b/machine/src/workflow-machine-interpreter.ts @@ -1,5 +1,5 @@ import { InterpreterStatus } from 'xstate'; -import { SequentialStateMachineInterpreter } from './types'; +import { SequentialStateMachineInterpreter, SignalPayload } from './types'; import { WorkflowMachineSnapshot } from './workflow-machine-snapshot'; export class WorkflowMachineInterpreter { @@ -37,8 +37,10 @@ export class WorkflowMachineInterpreter { return false; } - public sendSignal(signalName: string, params?: Record): this { - this.interpreter.send(signalName, params); + public sendSignal

(event: string, payload: P): this { + this.interpreter.send(event, { + payload + }); return this; } diff --git a/machine/src/workflow-machine.ts b/machine/src/workflow-machine.ts index 96dc861..ff2ba61 100644 --- a/machine/src/workflow-machine.ts +++ b/machine/src/workflow-machine.ts @@ -8,7 +8,10 @@ export interface StartConfig { } export class WorkflowMachine { - public constructor(private readonly definition: Definition, private readonly machine: SequentialStateMachine) {} + public constructor( + private readonly definition: Definition, + private readonly machine: SequentialStateMachine + ) {} public create(config: StartConfig): WorkflowMachineInterpreter { return this.restore({ diff --git a/yarn.lock b/yarn.lock index 9faa179..fa3c30f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2485,10 +2485,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.8.4: - version "2.8.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" - integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== +prettier@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== pretty-format@^29.0.0, pretty-format@^29.4.3: version "29.4.3"