Skip to content

Commit 2bb0159

Browse files
authored
0.7.1. (#14)
1 parent 89923f8 commit 2bb0159

7 files changed

+160
-9
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.7.1
2+
3+
This version introduces experimental support for workflow persistence. Please refer to the `machine/src/activities/signal-activity/signal-activity-persistence.spec.ts` file.
4+
15
## 0.7.0
26

37
This version introduces the signal activity. The signal activity stops the execution of the workflow machine and waits for a signal.

machine/package.json

+1-1
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.7.0",
4+
"version": "0.7.1",
55
"type": "module",
66
"main": "./lib/esm/index.js",
77
"types": "./lib/index.d.ts",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 { createAtomActivityFromHandler } from '../atom-activity';
6+
import { SerializedWorkflowMachineSnapshot } from '../../types';
7+
8+
interface TestGlobalState {
9+
logs: string[];
10+
}
11+
12+
const activitySet = createActivitySet<TestGlobalState>([
13+
createAtomActivityFromHandler<Step, TestGlobalState>('ping', async (step, globalState) => {
14+
globalState.logs.push(`ping:${step.name}`);
15+
}),
16+
createSignalActivity<Step, TestGlobalState>('waitForSignal', {
17+
init: () => ({}),
18+
beforeSignal: async (step, globalState) => {
19+
globalState.logs.push(`beforeSignal:${step.name}`);
20+
},
21+
afterSignal: async (step, globalState) => {
22+
globalState.logs.push(`afterSignal:${step.name}`);
23+
}
24+
})
25+
]);
26+
27+
function createPingStep(id: string, name: string): Step {
28+
return {
29+
id,
30+
componentType: 'task',
31+
type: 'ping',
32+
name,
33+
properties: {}
34+
};
35+
}
36+
37+
describe('SignalActivity - persistence', () => {
38+
it('saves state, restores state', done => {
39+
const definition: Definition = {
40+
sequence: [
41+
createPingStep('0x1', 'p1'),
42+
{
43+
id: '0x2',
44+
componentType: 'task',
45+
type: 'waitForSignal',
46+
name: 'w1',
47+
properties: {}
48+
},
49+
createPingStep('0x3', 'p2'),
50+
createPingStep('0x4', 'p3')
51+
],
52+
properties: {}
53+
};
54+
55+
const builder = createWorkflowMachineBuilder(activitySet);
56+
const machine = builder.build(definition);
57+
58+
let isIsInstance1Stopped = false;
59+
60+
function runInstance2(serializedSnapshot: SerializedWorkflowMachineSnapshot<TestGlobalState>) {
61+
const size = JSON.stringify(serializedSnapshot).length;
62+
63+
expect(size).toBe(2395);
64+
65+
const interpreter = machine.deserializeSnapshot(serializedSnapshot);
66+
const paths: (string[] | null)[] = [];
67+
68+
interpreter.onChange(() => {
69+
const path = interpreter.getSnapshot().tryGetStatePath();
70+
paths.push(path);
71+
if (path && path.includes('WAIT_FOR_SIGNAL')) {
72+
expect(paths.length).toBe(1); // First path should be the same as the one from the serialized snapshot
73+
signalSignalActivity(interpreter, {});
74+
}
75+
});
76+
interpreter.onDone(() => {
77+
const snapshot = interpreter.getSnapshot();
78+
expect(isIsInstance1Stopped).toBe(true);
79+
expect(snapshot.globalState.logs).toEqual(['ping:p1', 'beforeSignal:w1', 'afterSignal:w1', 'ping:p2', 'ping:p3']);
80+
expect(paths).toStrictEqual([
81+
['MAIN', 'STEP_0x2', 'WAIT_FOR_SIGNAL'],
82+
['MAIN', 'STEP_0x2', 'AFTER_SIGNAL'],
83+
['MAIN', 'STEP_0x3'],
84+
['MAIN', 'STEP_0x4'],
85+
['FINISHED']
86+
]);
87+
done();
88+
});
89+
interpreter.start();
90+
}
91+
92+
function runInstance1() {
93+
const interpreter = machine.create({
94+
init: () => ({
95+
logs: []
96+
})
97+
});
98+
const paths: (string[] | null)[] = [];
99+
100+
interpreter.onChange(() => {
101+
const snapshot = interpreter.getSnapshot();
102+
const path = snapshot.tryGetStatePath();
103+
paths.push(path);
104+
105+
if (path && path.includes('WAIT_FOR_SIGNAL')) {
106+
expect(snapshot.globalState.logs).toEqual(['ping:p1', 'beforeSignal:w1']);
107+
expect(paths).toStrictEqual([
108+
['MAIN', 'STEP_0x1'],
109+
['MAIN', 'STEP_0x2', 'BEFORE_SIGNAL'],
110+
['MAIN', 'STEP_0x2', 'WAIT_FOR_SIGNAL']
111+
]);
112+
runInstance2(interpreter.serializeSnapshot());
113+
expect(interpreter.isRunning()).toBe(true);
114+
expect(interpreter.tryStop()).toBe(true);
115+
}
116+
});
117+
interpreter.onDone(() => {
118+
const snapshot = interpreter.getSnapshot();
119+
const path = snapshot.tryGetStatePath();
120+
expect(path).toStrictEqual(['MAIN', 'STEP_0x2', 'WAIT_FOR_SIGNAL']);
121+
isIsInstance1Stopped = true;
122+
});
123+
interpreter.start();
124+
}
125+
126+
runInstance1();
127+
});
128+
});

machine/src/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EventObject, Interpreter, StateMachine, StateNodeConfig, StateSchema, Typestate } from 'xstate';
1+
import { EventObject, Interpreter, StateConfig, StateMachine, StateNodeConfig, StateSchema, Typestate } from 'xstate';
22
import { SequenceNodeBuilder } from './core/sequence-node-builder';
33
import { Definition, Step } from 'sequential-workflow-model';
44
import { MachineUnhandledError } from './machine-unhandled-error';
@@ -57,3 +57,5 @@ export type SequentialStateMachineInterpreter<TGlobalState> = Interpreter<
5757
>;
5858

5959
export type SignalPayload = Record<string, unknown>;
60+
61+
export type SerializedWorkflowMachineSnapshot<TGlobalState> = StateConfig<MachineContext<TGlobalState>, EventObject>;

machine/src/workflow-machine-builder.ts

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { WorkflowMachine } from './workflow-machine';
77
import { getStepNodeId } from './core';
88

99
export interface BuildConfig {
10+
/**
11+
* @deprecated This property will be removed in the next minor version.
12+
*/
1013
initialStepId?: string;
1114
}
1215

machine/src/workflow-machine-interpreter.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { InterpreterStatus } from 'xstate';
2-
import { SequentialStateMachineInterpreter, SignalPayload } from './types';
1+
import { InterpreterStatus, State } from 'xstate';
2+
import { MachineContext, SequentialStateMachineInterpreter, SerializedWorkflowMachineSnapshot, SignalPayload } from './types';
33
import { WorkflowMachineSnapshot } from './workflow-machine-snapshot';
44

55
export class WorkflowMachineInterpreter<GlobalState> {
6-
public constructor(private readonly interpreter: SequentialStateMachineInterpreter<GlobalState>) {}
6+
public constructor(
7+
private readonly interpreter: SequentialStateMachineInterpreter<GlobalState>,
8+
private readonly initState: State<MachineContext<GlobalState>> | undefined
9+
) {}
710

811
public start(): this {
9-
this.interpreter.start();
12+
this.interpreter.start(this.initState);
1013
return this;
1114
}
1215

@@ -15,6 +18,10 @@ export class WorkflowMachineInterpreter<GlobalState> {
1518
return new WorkflowMachineSnapshot(snapshot.context.globalState, snapshot.context.unhandledError, snapshot.value);
1619
}
1720

21+
public serializeSnapshot(): SerializedWorkflowMachineSnapshot<GlobalState> {
22+
return this.interpreter.getSnapshot().toJSON() as unknown as SerializedWorkflowMachineSnapshot<GlobalState>;
23+
}
24+
1825
public onDone(callback: () => void): this {
1926
this.interpreter.onStop(callback);
2027
return this;

machine/src/workflow-machine.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Definition } from 'sequential-workflow-model';
2-
import { interpret } from 'xstate';
3-
import { GlobalStateInitializer, MachineContext, SequentialStateMachine } from './types';
2+
import { interpret, State } from 'xstate';
3+
import { GlobalStateInitializer, MachineContext, SequentialStateMachine, SerializedWorkflowMachineSnapshot } from './types';
44
import { WorkflowMachineInterpreter } from './workflow-machine-interpreter';
55

66
export interface StartConfig<GlobalState> {
@@ -22,7 +22,14 @@ export class WorkflowMachine<GlobalState> {
2222

2323
public restore(context: MachineContext<GlobalState>): WorkflowMachineInterpreter<GlobalState> {
2424
const machine = this.machine.withContext(context);
25-
return new WorkflowMachineInterpreter(interpret(machine));
25+
return new WorkflowMachineInterpreter(interpret(machine), undefined);
26+
}
27+
28+
public deserializeSnapshot(
29+
serializedSnapshot: SerializedWorkflowMachineSnapshot<GlobalState>
30+
): WorkflowMachineInterpreter<GlobalState> {
31+
const initState = this.machine.resolveState(State.create(serializedSnapshot));
32+
return new WorkflowMachineInterpreter(interpret(this.machine), initState as unknown as State<MachineContext<GlobalState>>);
2633
}
2734

2835
public getNative(): SequentialStateMachine<GlobalState> {

0 commit comments

Comments
 (0)