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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 1 addition & 0 deletions
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';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './signal-activity';
2+
export * from './types';
Lines changed: 85 additions & 0 deletions
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+
}
Lines changed: 81 additions & 0 deletions
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+
});
Lines changed: 22 additions & 0 deletions
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+
}
Lines changed: 24 additions & 0 deletions
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+
}
Lines changed: 5 additions & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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>;

0 commit comments

Comments
 (0)