Skip to content

Commit c945fac

Browse files
authored
0.5.0. (#7)
1 parent 63d598f commit c945fac

19 files changed

+226
-49
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.5.0
2+
3+
This version simplifies error handling:
4+
5+
* The `getSnapshot()` method now returns an instance of the `WorkflowMachineSnapshot` class, which includes three new methods: `isFinished()`, `isFailed()`, and `isInterrupted()`. Additionally, you can retrieve the id of the last executing step by calling the `tryGetCurrentStepId()` method.
6+
* The `unhandledError` property of the snapshot class is always an instance of the `MachineUnhandledError` class.
7+
18
## 0.4.0
29

310
Updated the `sequential-workflow-model` dependency to the version `0.2.0`.

machine/package.json

Lines changed: 1 addition & 1 deletion
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.4.0",
4+
"version": "0.5.0",
55
"type": "module",
66
"main": "./lib/esm/index.js",
77
"types": "./lib/index.d.ts",

machine/src/activities/atom-activity/atom-activity.spec.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { createAtomActivity, createAtomActivityFromHandler } from './atom-activity';
22
import { createActivitySet } from '../../core/activity-set';
33
import { createWorkflowMachineBuilder } from '../../workflow-machine-builder';
4-
import { STATE_FINISHED_ID, STATE_INTERRUPTED_ID, STATE_FAILED_ID } from '../../types';
54
import { Definition, Step } from 'sequential-workflow-model';
65
import { interrupt } from '../results/interrupt-result';
76

@@ -72,7 +71,9 @@ describe('AtomActivity', () => {
7271
interpreter.onDone(() => {
7372
const snapshot = interpreter.getSnapshot();
7473

75-
expect(snapshot.statePath[0]).toBe(STATE_FINISHED_ID);
74+
expect(snapshot.isFinished()).toBe(true);
75+
expect(snapshot.isFailed()).toBe(false);
76+
expect(snapshot.isInterrupted()).toBe(false);
7677
expect(snapshot.globalState.counter).toBe(20);
7778

7879
done();
@@ -99,7 +100,9 @@ describe('AtomActivity', () => {
99100
interpreter.onDone(() => {
100101
const snapshot = interpreter.getSnapshot();
101102

102-
expect(snapshot.statePath[0]).toBe(STATE_INTERRUPTED_ID);
103+
expect(snapshot.isInterrupted()).toBe(true);
104+
expect(snapshot.isFailed()).toBe(false);
105+
expect(snapshot.isFinished()).toBe(false);
103106
expect(snapshot.globalState.counter).toBe(0);
104107

105108
done();
@@ -126,9 +129,12 @@ describe('AtomActivity', () => {
126129
interpreter.onDone(() => {
127130
const snapshot = interpreter.getSnapshot();
128131

129-
expect(snapshot.statePath[0]).toBe(STATE_FAILED_ID);
132+
expect(snapshot.isFailed()).toBe(true);
133+
expect(snapshot.isFinished()).toBe(false);
134+
expect(snapshot.isInterrupted()).toBe(false);
130135
expect(snapshot.unhandledError).toBeInstanceOf(Error);
131-
expect((snapshot.unhandledError as Error).message).toBe('TEST_ERROR');
136+
expect(snapshot.unhandledError?.message).toBe('TEST_ERROR');
137+
expect(snapshot.unhandledError?.stepId).toBe('0x2');
132138

133139
done();
134140
});

machine/src/activities/break-activity/break-activity.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { createBreakActivity } from './break-activity';
44
import { createActivitySet } from '../../core';
55
import { createAtomActivity } from '../atom-activity';
66
import { createWorkflowMachineBuilder } from '../../workflow-machine-builder';
7-
import { STATE_FINISHED_ID } from '../../types';
87
import { break_ } from './break-result';
98

109
interface TestGlobalState {
@@ -98,7 +97,7 @@ describe('BreakActivity', () => {
9897
interpreter.onDone(() => {
9998
const snapshot = interpreter.getSnapshot();
10099

101-
expect(snapshot.statePath[0]).toBe(STATE_FINISHED_ID);
100+
expect(snapshot.isFinished()).toBe(true);
102101
expect(snapshot.globalState.trace).toBe(
103102
'(condition)(decrement)(break)(condition)(decrement)(break)(condition)(decrement)(break)'
104103
);

machine/src/activities/container-activity/container-activity.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createActivitySet } from '../../core/activity-set';
22
import { createWorkflowMachineBuilder } from '../../workflow-machine-builder';
3-
import { STATE_FINISHED_ID } from '../../types';
43
import { Definition, SequentialStep, Step } from 'sequential-workflow-model';
54
import { createContainerActivity } from './container-activity';
65
import { createAtomActivity } from '../atom-activity';
@@ -71,7 +70,9 @@ describe('ContainerActivity', () => {
7170
.onDone(() => {
7271
const snapshot = interpreter.getSnapshot();
7372

74-
expect(snapshot.statePath[0]).toBe(STATE_FINISHED_ID);
73+
expect(snapshot.isFinished()).toBe(true);
74+
expect(snapshot.isInterrupted()).toBe(false);
75+
expect(snapshot.isFailed()).toBe(false);
7576
expect(snapshot.globalState.counter).toBe(2);
7677
expect(snapshot.globalState.entered).toBe(true);
7778
expect(snapshot.globalState.leaved).toBe(true);

machine/src/activities/fork-activity/fork-activity.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { createAtomActivity } from '../atom-activity/atom-activity';
22
import { createActivitySet } from '../../core/activity-set';
33
import { createWorkflowMachineBuilder } from '../../workflow-machine-builder';
44
import { createForkActivity } from './fork-activity';
5-
import { STATE_FAILED_ID, STATE_FINISHED_ID, STATE_INTERRUPTED_ID } from '../../types';
65
import { BranchedStep, Definition, Step } from 'sequential-workflow-model';
76
import { interrupt } from '../results/interrupt-result';
87
import { branchName } from '../results/branch-name-result';
@@ -105,7 +104,7 @@ describe('ForkActivity', () => {
105104
interpreter.onDone(() => {
106105
const snapshot = interpreter.getSnapshot();
107106

108-
expect(snapshot.statePath[0]).toBe(STATE_FINISHED_ID);
107+
expect(snapshot.isFinished()).toBe(true);
109108
expect(snapshot.globalState.message).toBe('(start)(true)(end)');
110109

111110
done();
@@ -124,7 +123,7 @@ describe('ForkActivity', () => {
124123
interpreter.onDone(() => {
125124
const snapshot = interpreter.getSnapshot();
126125

127-
expect(snapshot.statePath[0]).toBe(STATE_INTERRUPTED_ID);
126+
expect(snapshot.isInterrupted()).toBe(true);
128127
expect(snapshot.globalState.message).toBe('(start)');
129128

130129
done();
@@ -143,8 +142,9 @@ describe('ForkActivity', () => {
143142
interpreter.onDone(() => {
144143
const snapshot = interpreter.getSnapshot();
145144

146-
expect(snapshot.statePath[0]).toBe(STATE_FAILED_ID);
147-
expect((snapshot.unhandledError as Error).message).toBe('TEST_ERROR');
145+
expect(snapshot.isFailed()).toBe(true);
146+
expect(snapshot.unhandledError?.message).toBe('TEST_ERROR');
147+
expect(snapshot.unhandledError?.stepId).toBe('0x002');
148148
expect(snapshot.globalState.message).toBe('(start)');
149149

150150
done();

machine/src/activities/interruption-activity/interruption-activity.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createActivitySet } from '../../core/activity-set';
22
import { createWorkflowMachineBuilder } from '../../workflow-machine-builder';
3-
import { STATE_INTERRUPTED_ID } from '../../types';
43
import { Definition, Step } from 'sequential-workflow-model';
54
import { createInterruptionActivity } from './interruption-activity';
65

@@ -44,7 +43,9 @@ describe('InterruptionActivity', () => {
4443
interpreter.onDone(() => {
4544
const snapshot = interpreter.getSnapshot();
4645

47-
expect(snapshot.statePath[0]).toBe(STATE_INTERRUPTED_ID);
46+
expect(snapshot.isInterrupted()).toBe(true);
47+
expect(snapshot.isFinished()).toBe(false);
48+
expect(snapshot.isFailed()).toBe(false);
4849
expect(snapshot.globalState.called).toBe(true);
4950

5051
done();

machine/src/activities/loop-activity/loop-activity.spec.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ function createTaskStep(id: string): Step {
2121

2222
const definition: Definition = {
2323
sequence: [
24-
createTaskStep('0x001'),
24+
createTaskStep('task_a'),
2525
{
26-
id: '0x002',
26+
id: 'loop',
2727
componentType: 'container',
2828
name: 'Loop',
2929
type: 'loop',
3030
properties: {},
31-
sequence: [createTaskStep('0x003')]
31+
sequence: [createTaskStep('task_b')]
3232
} as SequentialStep,
33-
createTaskStep('0x004')
33+
createTaskStep('task_c')
3434
],
3535
properties: {}
3636
};
@@ -73,6 +73,23 @@ const machine = builder.build(definition);
7373

7474
describe('LoopActivity', () => {
7575
it('should iterate', done => {
76+
const expectedRun = [
77+
{ id: 'loop', path: ['MAIN', 'STEP_loop', 'ENTER'] },
78+
{ id: 'loop', path: ['MAIN', 'STEP_loop', 'CONDITION'] },
79+
{
80+
id: 'task_b',
81+
path: ['MAIN', 'STEP_loop', 'LOOP', 'STEP_task_b']
82+
},
83+
{ id: 'loop', path: ['MAIN', 'STEP_loop', 'CONDITION'] },
84+
{
85+
id: 'task_b',
86+
path: ['MAIN', 'STEP_loop', 'LOOP', 'STEP_task_b']
87+
},
88+
{ id: 'loop', path: ['MAIN', 'STEP_loop', 'CONDITION'] },
89+
{ id: 'loop', path: ['MAIN', 'STEP_loop', 'LEAVE'] },
90+
{ id: 'task_c', path: ['MAIN', 'STEP_task_c'] },
91+
{ id: null, path: ['FINISHED'] }
92+
];
7693
const interpreter = machine
7794
.create({
7895
init: () => ({
@@ -81,10 +98,23 @@ describe('LoopActivity', () => {
8198
})
8299
})
83100
.start();
101+
let index = 0;
84102

103+
interpreter.onChange(() => {
104+
const snapshot = interpreter.getSnapshot();
105+
expect(snapshot.getStatePath()).toMatchObject(expectedRun[index].path);
106+
expect(snapshot.tryGetCurrentStepId()).toBe(expectedRun[index].id);
107+
expect(snapshot.isFailed()).toBe(false);
108+
expect(snapshot.isFinished()).toBe(index === 8);
109+
expect(snapshot.isInterrupted()).toBe(false);
110+
index++;
111+
});
85112
interpreter.onDone(() => {
86-
const globalState = interpreter.getSnapshot().globalState;
113+
const snapshot = interpreter.getSnapshot();
114+
const globalState = snapshot.globalState;
87115

116+
expect(index).toBe(9);
117+
expect(snapshot.isFinished()).toBe(true);
88118
expect(globalState.counter).toBe(4);
89119
expect(globalState.trace).toBe('(onEnter)(condition)(condition)(condition)(onLeave)');
90120

machine/src/core/catch-unhandled-error.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { InvokeMeta } from 'xstate';
12
import { MachineContext } from '../types';
23
import { catchUnhandledError } from './catch-unhandled-error';
34

@@ -7,12 +8,16 @@ describe('catchUnhandledError()', () => {
78
activityStates: {},
89
globalState: {}
910
};
11+
const event = { type: 'x' };
12+
const meta = { src: { type: '(machine).MAIN.STEP_0x002.CONDITION:invocation[0]' } } as InvokeMeta;
1013

1114
catchUnhandledError(async () => {
1215
throw new Error('SOME_ERROR');
13-
})(context, { type: 'x' }).catch(e => {
16+
})(context, event, meta).catch(e => {
1417
expect((e as Error).message).toBe('SOME_ERROR');
15-
expect(context.unhandledError).toBe(e);
18+
expect(context.unhandledError?.cause).toBe(e);
19+
expect(context.unhandledError?.message).toBe('SOME_ERROR');
20+
expect(context.unhandledError?.stepId).toBe('0x002');
1621
done();
1722
});
1823
});

machine/src/core/catch-unhandled-error.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
import { EventObject } from 'xstate';
1+
import { EventObject, InvokeMeta } from 'xstate';
22
import { MachineContext } from '../types';
3+
import { MachineUnhandledError } from '../machine-unhandled-error';
4+
import { readMetaPath } from './meta-path-reader';
35

4-
export function catchUnhandledError<TGlobalState>(callback: (context: MachineContext<TGlobalState>, event: EventObject) => Promise<void>) {
5-
return async (context: MachineContext<TGlobalState>, event: EventObject) => {
6+
export function catchUnhandledError<TGlobalState>(
7+
callback: (context: MachineContext<TGlobalState>, event: EventObject, meta: InvokeMeta) => Promise<void>
8+
) {
9+
return async (context: MachineContext<TGlobalState>, event: EventObject, meta: InvokeMeta) => {
610
try {
7-
await callback(context, event);
11+
await callback(context, event, meta);
812
} catch (e) {
9-
context.unhandledError = e;
13+
const message = e instanceof Error ? e.message : String(e);
14+
const stepId = readMetaPath(meta);
15+
context.unhandledError = new MachineUnhandledError(message, e, stepId);
1016
throw e;
1117
}
1218
};

0 commit comments

Comments
 (0)