Skip to content

Commit 89923f8

Browse files
authored
0.7.0. (#13)
1 parent 6cd22e1 commit 89923f8

13 files changed

+242
-12
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.7.0
2+
3+
This version introduces the signal activity. The signal activity stops the execution of the workflow machine and waits for a signal.
4+
15
## 0.6.0
26

37
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.

machine/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sequential-workflow-machine",
33
"description": "Powerful sequential workflow machine for frontend and backend applications.",
4-
"version": "0.6.0",
4+
"version": "0.7.0",
55
"type": "module",
66
"main": "./lib/esm/index.js",
77
"types": "./lib/index.d.ts",
@@ -58,7 +58,7 @@
5858
"jest": "^29.4.3",
5959
"ts-jest": "^29.0.5",
6060
"typescript": "^4.9.5",
61-
"prettier": "^2.8.4",
61+
"prettier": "^3.3.3",
6262
"rollup": "^3.18.0",
6363
"rollup-plugin-dts": "^5.2.0",
6464
"rollup-plugin-typescript2": "^0.34.1",
@@ -72,4 +72,4 @@
7272
"nocode",
7373
"lowcode"
7474
]
75-
}
75+
}

machine/src/activities/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './interruption-activity';
66
export * from './loop-activity';
77
export * from './parallel-activity';
88
export * from './results';
9+
export * from './signal-activity';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './signal-activity';
2+
export * from './types';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { SignalActivityConfig } from './types';
2+
import { EventObject } from 'xstate';
3+
import { Step } from 'sequential-workflow-model';
4+
import { ActivityStateProvider, catchUnhandledError, getStepNodeId } from '../../core';
5+
import {
6+
ActivityNodeBuilder,
7+
ActivityNodeConfig,
8+
MachineContext,
9+
SignalPayload,
10+
STATE_FAILED_TARGET,
11+
STATE_INTERRUPTED_TARGET
12+
} from '../../types';
13+
import { isInterruptResult } from '../results';
14+
15+
export class SignalActivityNodeBuilder<TStep extends Step, TGlobalState, TActivityState extends object>
16+
implements ActivityNodeBuilder<TGlobalState>
17+
{
18+
public constructor(private readonly config: SignalActivityConfig<TStep, TGlobalState, TActivityState>) {}
19+
20+
public build(step: TStep, nextNodeTarget: string): ActivityNodeConfig<TGlobalState> {
21+
const activityStateProvider = new ActivityStateProvider(step, this.config.init);
22+
const nodeId = getStepNodeId(step.id);
23+
24+
return {
25+
id: nodeId,
26+
initial: 'BEFORE_SIGNAL',
27+
states: {
28+
BEFORE_SIGNAL: {
29+
invoke: {
30+
src: catchUnhandledError(async (context: MachineContext<TGlobalState>) => {
31+
const activityState = activityStateProvider.get(context, nodeId);
32+
33+
const result = await this.config.beforeSignal(step, context.globalState, activityState);
34+
if (isInterruptResult(result)) {
35+
context.interrupted = nodeId;
36+
return;
37+
}
38+
}),
39+
onDone: [
40+
{
41+
target: STATE_INTERRUPTED_TARGET,
42+
cond: (context: MachineContext<TGlobalState>) => Boolean(context.interrupted)
43+
},
44+
{
45+
target: 'WAIT_FOR_SIGNAL'
46+
}
47+
],
48+
onError: STATE_FAILED_TARGET
49+
}
50+
},
51+
WAIT_FOR_SIGNAL: {
52+
on: {
53+
SIGNAL_RECEIVED: {
54+
target: 'AFTER_SIGNAL'
55+
}
56+
}
57+
},
58+
AFTER_SIGNAL: {
59+
invoke: {
60+
src: catchUnhandledError(async (context: MachineContext<TGlobalState>, event: EventObject) => {
61+
const activityState = activityStateProvider.get(context, nodeId);
62+
const ev = event as { type: string; payload: SignalPayload };
63+
64+
const result = await this.config.afterSignal(step, context.globalState, activityState, ev.payload);
65+
if (isInterruptResult(result)) {
66+
context.interrupted = nodeId;
67+
return;
68+
}
69+
}),
70+
onDone: [
71+
{
72+
target: STATE_INTERRUPTED_TARGET,
73+
cond: (context: MachineContext<TGlobalState>) => Boolean(context.interrupted)
74+
},
75+
{
76+
target: nextNodeTarget
77+
}
78+
],
79+
onError: STATE_FAILED_TARGET
80+
}
81+
}
82+
}
83+
};
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Definition, Step } from 'sequential-workflow-model';
2+
import { createSignalActivity, signalSignalActivity } from './signal-activity';
3+
import { createActivitySet } from '../../core';
4+
import { createWorkflowMachineBuilder } from '../../workflow-machine-builder';
5+
import { STATE_FINISHED_ID } from '../../types';
6+
7+
interface TestGlobalState {
8+
beforeCalled: boolean;
9+
afterCalled: boolean;
10+
}
11+
12+
const activitySet = createActivitySet<TestGlobalState>([
13+
createSignalActivity<Step, TestGlobalState>('waitForSignal', {
14+
init: () => ({}),
15+
beforeSignal: async (_, globalState) => {
16+
expect(globalState.beforeCalled).toBe(false);
17+
expect(globalState.afterCalled).toBe(false);
18+
globalState.beforeCalled = true;
19+
},
20+
afterSignal: async (_, globalState, __, payload) => {
21+
expect(globalState.beforeCalled).toBe(true);
22+
expect(globalState.afterCalled).toBe(false);
23+
globalState.afterCalled = true;
24+
expect(payload['TEST_VALUE']).toBe(123456);
25+
expect(Object.keys(payload).length).toBe(1);
26+
}
27+
})
28+
]);
29+
30+
describe('SignalActivity', () => {
31+
it('stops, after signal continues', done => {
32+
const definition: Definition = {
33+
sequence: [
34+
{
35+
id: '0x1',
36+
componentType: 'task',
37+
type: 'waitForSignal',
38+
name: 'W8',
39+
properties: {}
40+
}
41+
],
42+
properties: {}
43+
};
44+
45+
const builder = createWorkflowMachineBuilder(activitySet);
46+
const machine = builder.build(definition);
47+
const interpreter = machine.create({
48+
init: () => ({
49+
afterCalled: false,
50+
beforeCalled: false
51+
})
52+
});
53+
54+
interpreter.onChange(() => {
55+
const snapshot = interpreter.getSnapshot();
56+
57+
if (snapshot.tryGetStatePath()?.includes('WAIT_FOR_SIGNAL')) {
58+
expect(snapshot.globalState.beforeCalled).toBe(true);
59+
expect(snapshot.globalState.afterCalled).toBe(false);
60+
61+
setTimeout(() => {
62+
signalSignalActivity(interpreter, {
63+
TEST_VALUE: 123456
64+
});
65+
}, 25);
66+
}
67+
});
68+
69+
interpreter.onDone(() => {
70+
const snapshot = interpreter.getSnapshot();
71+
72+
expect(snapshot.tryGetStatePath()).toStrictEqual([STATE_FINISHED_ID]);
73+
expect(snapshot.globalState.beforeCalled).toBe(true);
74+
expect(snapshot.globalState.afterCalled).toBe(true);
75+
76+
done();
77+
});
78+
79+
interpreter.start();
80+
});
81+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Activity, SignalPayload } from '../../types';
2+
import { WorkflowMachineInterpreter } from '../../workflow-machine-interpreter';
3+
import { SignalActivityNodeBuilder } from './signal-activity-node-builder';
4+
import { SignalActivityConfig } from './types';
5+
import { Step } from 'sequential-workflow-model';
6+
7+
export function createSignalActivity<TStep extends Step, GlobalState = object, TActivityState extends object = object>(
8+
stepType: TStep['type'],
9+
config: SignalActivityConfig<TStep, GlobalState, TActivityState>
10+
): Activity<GlobalState> {
11+
return {
12+
stepType,
13+
nodeBuilderFactory: () => new SignalActivityNodeBuilder(config)
14+
};
15+
}
16+
17+
export function signalSignalActivity<GlobalState, P extends SignalPayload>(
18+
interpreter: WorkflowMachineInterpreter<GlobalState>,
19+
payload: P
20+
) {
21+
interpreter.sendSignal('SIGNAL_RECEIVED', payload);
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Step } from 'sequential-workflow-model';
2+
import { ActivityStateInitializer, SignalPayload } from '../../types';
3+
import { InterruptResult } from '../results';
4+
5+
export type BeforeSignalActivityHandler<TStep extends Step, GlobalState, ActivityState> = (
6+
step: TStep,
7+
globalState: GlobalState,
8+
activityState: ActivityState
9+
) => Promise<SignalActivityHandlerResult>;
10+
11+
export type AfterSignalActivityHandler<TStep extends Step, GlobalState, ActivityState> = (
12+
step: TStep,
13+
globalState: GlobalState,
14+
activityState: ActivityState,
15+
signalPayload: SignalPayload
16+
) => Promise<SignalActivityHandlerResult>;
17+
18+
export type SignalActivityHandlerResult = void | InterruptResult;
19+
20+
export interface SignalActivityConfig<TStep extends Step, GlobalState, ActivityState extends object> {
21+
init: ActivityStateInitializer<TStep, GlobalState, ActivityState>;
22+
beforeSignal: BeforeSignalActivityHandler<TStep, GlobalState, ActivityState>;
23+
afterSignal: AfterSignalActivityHandler<TStep, GlobalState, ActivityState>;
24+
}
+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export class MachineUnhandledError extends Error {
2-
public constructor(message: string, public readonly cause: unknown, public readonly stepId: string | null) {
2+
public constructor(
3+
message: string,
4+
public readonly cause: unknown,
5+
public readonly stepId: string | null
6+
) {
37
super(message);
48
}
59
}

machine/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ export type SequentialStateMachineInterpreter<TGlobalState> = Interpreter<
5555
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5656
any
5757
>;
58+
59+
export type SignalPayload = Record<string, unknown>;

machine/src/workflow-machine-interpreter.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { InterpreterStatus } from 'xstate';
2-
import { SequentialStateMachineInterpreter } from './types';
2+
import { SequentialStateMachineInterpreter, SignalPayload } from './types';
33
import { WorkflowMachineSnapshot } from './workflow-machine-snapshot';
44

55
export class WorkflowMachineInterpreter<GlobalState> {
@@ -37,8 +37,10 @@ export class WorkflowMachineInterpreter<GlobalState> {
3737
return false;
3838
}
3939

40-
public sendSignal(signalName: string, params?: Record<string, unknown>): this {
41-
this.interpreter.send(signalName, params);
40+
public sendSignal<P extends SignalPayload>(event: string, payload: P): this {
41+
this.interpreter.send(event, {
42+
payload
43+
});
4244
return this;
4345
}
4446

machine/src/workflow-machine.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ export interface StartConfig<GlobalState> {
88
}
99

1010
export class WorkflowMachine<GlobalState> {
11-
public constructor(private readonly definition: Definition, private readonly machine: SequentialStateMachine<GlobalState>) {}
11+
public constructor(
12+
private readonly definition: Definition,
13+
private readonly machine: SequentialStateMachine<GlobalState>
14+
) {}
1215

1316
public create(config: StartConfig<GlobalState>): WorkflowMachineInterpreter<GlobalState> {
1417
return this.restore({

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -2485,10 +2485,10 @@ prelude-ls@^1.2.1:
24852485
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
24862486
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
24872487

2488-
prettier@^2.8.4:
2489-
version "2.8.4"
2490-
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
2491-
integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==
2488+
prettier@^3.3.3:
2489+
version "3.3.3"
2490+
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
2491+
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
24922492

24932493
pretty-format@^29.0.0, pretty-format@^29.4.3:
24942494
version "29.4.3"

0 commit comments

Comments
 (0)