Skip to content

Commit 1bdf249

Browse files
authored
0.1.3. (#1)
* 0.1.3.
1 parent 938a9e4 commit 1bdf249

19 files changed

+520
-10
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
name: main
22
on:
3+
pull_request:
4+
branches:
5+
- main
36
push:
47
branches:
58
- main
6-
- develop
79
jobs:
810
build:
911
name: Build

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.1.3
2+
3+
Added two new activities: `LoopActivity` and `BreakActivity`.
4+
15
## 0.1.2
26

37
Changed bundle format of the `sequential-workflow-machine` package to: UMD, ESM and CommonJS.

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.1.2",
4+
"version": "0.1.3",
55
"type": "module",
66
"main": "./lib/esm/index.js",
77
"types": "./lib/index.d.ts",
@@ -44,7 +44,7 @@
4444
},
4545
"peerDependencies": {
4646
"sequential-workflow-model": "^0.1.1",
47-
"xstate": "^4.37.0"
47+
"xstate": "^4.37.2"
4848
},
4949
"devDependencies": {
5050
"@types/jest": "^29.4.0",
@@ -60,7 +60,7 @@
6060
"rollup-plugin-typescript2": "^0.34.1",
6161
"@rollup/plugin-node-resolve": "^15.0.1",
6262
"sequential-workflow-model": "^0.1.1",
63-
"xstate": "^4.37.0"
63+
"xstate": "^4.37.2"
6464
},
6565
"keywords": [
6666
"workflow",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { BreakActivityConfig, BreakActivityState } from './types';
2+
import { getLoopStack } from '../loop-stack';
3+
import { Step } from 'sequential-workflow-model';
4+
import {
5+
ActivityNodeBuilder,
6+
ActivityNodeConfig,
7+
BuildingContext,
8+
MachineContext,
9+
STATE_FAILED_TARGET,
10+
STATE_INTERRUPTED_TARGET
11+
} from '../../types';
12+
import { ActivityStateAccessor, catchUnhandledError, getStepNodeId } from '../../core';
13+
import { isInterruptResult } from '../results';
14+
import { isBreakResult } from './break-result';
15+
16+
export class BreakActivityNodeBuilder<TStep extends Step, GlobalState, ActivityState> implements ActivityNodeBuilder<GlobalState> {
17+
public constructor(
18+
private readonly activityStateAccessor: ActivityStateAccessor<GlobalState, BreakActivityState<ActivityState>>,
19+
private readonly config: BreakActivityConfig<TStep, GlobalState, ActivityState>
20+
) {}
21+
22+
public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig<GlobalState> {
23+
const nodeId = getStepNodeId(step.id);
24+
25+
const loopName = this.config.loopName(step);
26+
const leaveNodeTarget = getLoopStack(buildingContext).getNodeTarget(loopName);
27+
28+
return {
29+
id: nodeId,
30+
invoke: {
31+
src: catchUnhandledError(async (context: MachineContext<GlobalState>) => {
32+
const internalState = this.activityStateAccessor.get(context, nodeId);
33+
34+
const result = await this.config.handler(step, context.globalState, internalState.activityState);
35+
if (isInterruptResult(result)) {
36+
context.interrupted = nodeId;
37+
return;
38+
}
39+
40+
internalState.break = isBreakResult(result);
41+
}),
42+
onDone: [
43+
{
44+
target: STATE_INTERRUPTED_TARGET,
45+
cond: (context: MachineContext<GlobalState>) => Boolean(context.interrupted)
46+
},
47+
{
48+
target: leaveNodeTarget,
49+
cond: (context: MachineContext<GlobalState>) => {
50+
const internalState = this.activityStateAccessor.get(context, nodeId);
51+
return Boolean(internalState.break);
52+
}
53+
},
54+
{
55+
target: nextNodeTarget
56+
}
57+
],
58+
onError: STATE_FAILED_TARGET
59+
}
60+
};
61+
}
62+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Definition, Sequence, SequentialStep, Step } from 'sequential-workflow-model';
2+
import { createLoopActivity } from '../loop-activity/loop-activity';
3+
import { createBreakActivity } from './break-activity';
4+
import { createActivitySet } from '../../core';
5+
import { createAtomActivity } from '../atom-activity';
6+
import { createWorkflowMachineBuilder } from '../../workflow-machine-builder';
7+
import { STATE_FINISHED_ID } from '../../types';
8+
import { break_ } from './break-result';
9+
10+
interface TestGlobalState {
11+
alfa: number;
12+
trace: string;
13+
}
14+
15+
function createLoopStep(id: string, loopName: string, sequence: Sequence): SequentialStep {
16+
return {
17+
id,
18+
componentType: 'container',
19+
type: 'loop',
20+
sequence,
21+
name: 'Loop',
22+
properties: {
23+
loopName
24+
}
25+
};
26+
}
27+
28+
function createDecrementStep(id: string): Step {
29+
return {
30+
id,
31+
componentType: 'task',
32+
type: 'decrement',
33+
name: 'Decrement',
34+
properties: {}
35+
};
36+
}
37+
38+
function createBreakIfZeroStep(id: string, loopName: string): Step {
39+
return {
40+
id,
41+
componentType: 'task',
42+
type: 'breakIfZero',
43+
name: 'Break',
44+
properties: {
45+
loopName
46+
}
47+
};
48+
}
49+
50+
const definition: Definition = {
51+
sequence: [createLoopStep('0x001', 'LOOP_1', [createDecrementStep('0x002'), createBreakIfZeroStep('0x003', 'LOOP_1')])],
52+
properties: {}
53+
};
54+
55+
const activitySet = createActivitySet<TestGlobalState>([
56+
createLoopActivity<SequentialStep, TestGlobalState>({
57+
stepType: 'loop',
58+
loopName: step => String(step.properties['loopName']),
59+
init: () => ({}),
60+
condition: async (_, globalState) => {
61+
globalState.trace += '(condition)';
62+
return true;
63+
}
64+
}),
65+
66+
createAtomActivity({
67+
stepType: 'decrement',
68+
init: () => null,
69+
handler: async (_, globalState) => {
70+
globalState.trace += '(decrement)';
71+
globalState.alfa--;
72+
}
73+
}),
74+
75+
createBreakActivity({
76+
stepType: 'breakIfZero',
77+
init: () => null,
78+
handler: async (_, globalState) => {
79+
globalState.trace += '(break)';
80+
if (globalState.alfa === 0) {
81+
return break_();
82+
}
83+
},
84+
loopName: step => String(step.properties['loopName'])
85+
})
86+
]);
87+
88+
describe('BreakActivity', () => {
89+
it('should break loop', done => {
90+
const builder = createWorkflowMachineBuilder(activitySet);
91+
const machine = builder.build(definition);
92+
const interpreter = machine
93+
.create({
94+
init: () => ({
95+
alfa: 3,
96+
trace: ''
97+
})
98+
})
99+
.start();
100+
101+
interpreter.onDone(() => {
102+
const snapshot = interpreter.getSnapshot();
103+
104+
expect(snapshot.statePath[0]).toBe(STATE_FINISHED_ID);
105+
expect(snapshot.globalState.trace).toBe(
106+
'(condition)(decrement)(break)(condition)(decrement)(break)(condition)(decrement)(break)'
107+
);
108+
expect(snapshot.globalState.alfa).toBe(0);
109+
110+
done();
111+
});
112+
});
113+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { BreakActivityConfig, BreakActivityState } from './types';
2+
import { BreakActivityNodeBuilder } from './break-activity-node-builder';
3+
import { Step } from 'sequential-workflow-model';
4+
import { Activity } from '../../types';
5+
import { ActivityStateAccessor } from '../../core';
6+
7+
export function createBreakActivity<TStep extends Step = Step, GlobalState = object, ActivityState = object>(
8+
config: BreakActivityConfig<TStep, GlobalState, ActivityState>
9+
): Activity<GlobalState> {
10+
const activityStateAccessor = new ActivityStateAccessor<GlobalState, BreakActivityState<ActivityState>>(globalState => ({
11+
activityState: config.init(globalState)
12+
}));
13+
14+
return {
15+
stepType: config.stepType,
16+
nodeBuilderFactory: () => new BreakActivityNodeBuilder(activityStateAccessor, config)
17+
};
18+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface BreakResult {
2+
break: true;
3+
}
4+
5+
export function isBreakResult(result: unknown): result is BreakResult {
6+
return typeof result === 'object' && (result as BreakResult).break;
7+
}
8+
9+
export function break_(): BreakResult {
10+
return { break: true };
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './break-activity';
2+
export * from './break-result';
3+
export * from './types';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Step } from 'sequential-workflow-model';
2+
import { ActivityConfig, ActivityStateInitializer } from '../../types';
3+
import { InterruptResult } from '../results';
4+
import { BreakResult } from './break-result';
5+
6+
export type BreakActivityHandler<TStep extends Step, GlobalState, ActivityState> = (
7+
step: TStep,
8+
globalState: GlobalState,
9+
activityState: ActivityState
10+
) => Promise<BreakActivityHandlerResult>;
11+
12+
export type BreakActivityHandlerResult = void | InterruptResult | BreakResult;
13+
14+
export interface BreakActivityConfig<TStep extends Step, GlobalState, ActivityState> extends ActivityConfig<TStep> {
15+
loopName: (step: TStep) => string;
16+
init: ActivityStateInitializer<GlobalState, ActivityState>;
17+
handler: BreakActivityHandler<TStep, GlobalState, ActivityState>;
18+
}
19+
20+
export interface BreakActivityState<ActivityState> {
21+
break?: boolean;
22+
activityState: ActivityState;
23+
}

machine/src/activities/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export * from './atom-activity';
2+
export * from './break-activity';
23
export * from './container-activity';
34
export * from './fork-activity';
45
export * from './interruption-activity';
6+
export * from './loop-activity';
57
export * from './results';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './loop-activity';
2+
export * from './types';
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { SequentialStep } from 'sequential-workflow-model';
2+
import { getLoopStack } from '../loop-stack';
3+
import { LoopActivityConfig, LoopActivityState } from './types';
4+
import { ActivityStateAccessor, SequenceNodeBuilder, catchUnhandledError, getStepNodeId } from '../../core';
5+
import {
6+
ActivityNodeBuilder,
7+
ActivityNodeConfig,
8+
BuildingContext,
9+
MachineContext,
10+
STATE_FAILED_TARGET,
11+
STATE_INTERRUPTED_TARGET
12+
} from '../../types';
13+
import { isInterruptResult } from '../results';
14+
15+
export class LoopActivityNodeBuilder<TStep extends SequentialStep, GlobalState, ActivityState> implements ActivityNodeBuilder<GlobalState> {
16+
public constructor(
17+
private readonly sequenceNodeBuilder: SequenceNodeBuilder<GlobalState>,
18+
private readonly activityStateAccessor: ActivityStateAccessor<GlobalState, LoopActivityState<ActivityState>>,
19+
private readonly config: LoopActivityConfig<TStep, GlobalState, ActivityState>
20+
) {}
21+
22+
public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig<GlobalState> {
23+
const nodeId = getStepNodeId(step.id);
24+
25+
const conditionNodeId = `CONDITION.${nodeId}`;
26+
const conditionNodeTarget = `#${conditionNodeId}`;
27+
const leaveNodeId = `LEAVE.${nodeId}`;
28+
const leaveNodeTarget = `#${leaveNodeId}`;
29+
30+
const loopName = this.config.loopName(step);
31+
const loopStack = getLoopStack(buildingContext);
32+
loopStack.push(loopName, leaveNodeTarget);
33+
34+
const LOOP = this.sequenceNodeBuilder.build(buildingContext, step.sequence, conditionNodeTarget);
35+
36+
loopStack.pop();
37+
38+
return {
39+
id: nodeId,
40+
initial: 'ENTER',
41+
states: {
42+
ENTER: {
43+
invoke: {
44+
src: catchUnhandledError(async (context: MachineContext<GlobalState>) => {
45+
if (this.config.onEnter) {
46+
const internalContext = this.activityStateAccessor.get(context, nodeId);
47+
this.config.onEnter(step, context.globalState, internalContext.activityState);
48+
}
49+
}),
50+
onDone: 'CONDITION',
51+
onError: STATE_FAILED_TARGET
52+
}
53+
},
54+
CONDITION: {
55+
id: conditionNodeId,
56+
invoke: {
57+
src: catchUnhandledError(async (context: MachineContext<GlobalState>) => {
58+
const internalState = this.activityStateAccessor.get(context, nodeId);
59+
60+
const result = await this.config.condition(step, context.globalState, internalState.activityState);
61+
if (isInterruptResult(result)) {
62+
context.interrupted = nodeId;
63+
return;
64+
}
65+
66+
internalState.continue = result;
67+
}),
68+
onDone: [
69+
{
70+
target: STATE_INTERRUPTED_TARGET,
71+
cond: (context: MachineContext<GlobalState>) => Boolean(context.interrupted)
72+
},
73+
{
74+
target: 'LOOP',
75+
cond: (context: MachineContext<GlobalState>) => {
76+
const activityState = this.activityStateAccessor.get(context, nodeId);
77+
return Boolean(activityState.continue);
78+
}
79+
},
80+
{
81+
target: 'LEAVE'
82+
}
83+
],
84+
onError: STATE_FAILED_TARGET
85+
}
86+
},
87+
LOOP,
88+
LEAVE: {
89+
id: leaveNodeId,
90+
invoke: {
91+
src: catchUnhandledError(async (context: MachineContext<GlobalState>) => {
92+
if (this.config.onLeave) {
93+
const internalState = this.activityStateAccessor.get(context, nodeId);
94+
this.config.onLeave(step, context.globalState, internalState.activityState);
95+
}
96+
}),
97+
onDone: nextNodeTarget,
98+
onError: STATE_FAILED_TARGET
99+
}
100+
}
101+
}
102+
};
103+
}
104+
}

0 commit comments

Comments
 (0)