Skip to content

0.7.1. #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.7.1

This version introduces experimental support for workflow persistence. Please refer to the `machine/src/activities/signal-activity/signal-activity-persistence.spec.ts` file.

## 0.7.0

This version introduces the signal activity. The signal activity stops the execution of the workflow machine and waits for a signal.
Expand Down
2 changes: 1 addition & 1 deletion machine/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sequential-workflow-machine",
"description": "Powerful sequential workflow machine for frontend and backend applications.",
"version": "0.7.0",
"version": "0.7.1",
"type": "module",
"main": "./lib/esm/index.js",
"types": "./lib/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Definition, Step } from 'sequential-workflow-model';
import { createSignalActivity, signalSignalActivity } from './signal-activity';
import { createActivitySet } from '../../core';
import { createWorkflowMachineBuilder } from '../../workflow-machine-builder';
import { createAtomActivityFromHandler } from '../atom-activity';
import { SerializedWorkflowMachineSnapshot } from '../../types';

interface TestGlobalState {
logs: string[];
}

const activitySet = createActivitySet<TestGlobalState>([
createAtomActivityFromHandler<Step, TestGlobalState>('ping', async (step, globalState) => {
globalState.logs.push(`ping:${step.name}`);
}),
createSignalActivity<Step, TestGlobalState>('waitForSignal', {
init: () => ({}),
beforeSignal: async (step, globalState) => {
globalState.logs.push(`beforeSignal:${step.name}`);
},
afterSignal: async (step, globalState) => {
globalState.logs.push(`afterSignal:${step.name}`);
}
})
]);

function createPingStep(id: string, name: string): Step {
return {
id,
componentType: 'task',
type: 'ping',
name,
properties: {}
};
}

describe('SignalActivity - persistence', () => {
it('saves state, restores state', done => {
const definition: Definition = {
sequence: [
createPingStep('0x1', 'p1'),
{
id: '0x2',
componentType: 'task',
type: 'waitForSignal',
name: 'w1',
properties: {}
},
createPingStep('0x3', 'p2'),
createPingStep('0x4', 'p3')
],
properties: {}
};

const builder = createWorkflowMachineBuilder(activitySet);
const machine = builder.build(definition);

let isIsInstance1Stopped = false;

function runInstance2(serializedSnapshot: SerializedWorkflowMachineSnapshot<TestGlobalState>) {
const size = JSON.stringify(serializedSnapshot).length;

expect(size).toBe(2395);

const interpreter = machine.deserializeSnapshot(serializedSnapshot);
const paths: (string[] | null)[] = [];

interpreter.onChange(() => {
const path = interpreter.getSnapshot().tryGetStatePath();
paths.push(path);
if (path && path.includes('WAIT_FOR_SIGNAL')) {
expect(paths.length).toBe(1); // First path should be the same as the one from the serialized snapshot
signalSignalActivity(interpreter, {});
}
});
interpreter.onDone(() => {
const snapshot = interpreter.getSnapshot();
expect(isIsInstance1Stopped).toBe(true);
expect(snapshot.globalState.logs).toEqual(['ping:p1', 'beforeSignal:w1', 'afterSignal:w1', 'ping:p2', 'ping:p3']);
expect(paths).toStrictEqual([
['MAIN', 'STEP_0x2', 'WAIT_FOR_SIGNAL'],
['MAIN', 'STEP_0x2', 'AFTER_SIGNAL'],
['MAIN', 'STEP_0x3'],
['MAIN', 'STEP_0x4'],
['FINISHED']
]);
done();
});
interpreter.start();
}

function runInstance1() {
const interpreter = machine.create({
init: () => ({
logs: []
})
});
const paths: (string[] | null)[] = [];

interpreter.onChange(() => {
const snapshot = interpreter.getSnapshot();
const path = snapshot.tryGetStatePath();
paths.push(path);

if (path && path.includes('WAIT_FOR_SIGNAL')) {
expect(snapshot.globalState.logs).toEqual(['ping:p1', 'beforeSignal:w1']);
expect(paths).toStrictEqual([
['MAIN', 'STEP_0x1'],
['MAIN', 'STEP_0x2', 'BEFORE_SIGNAL'],
['MAIN', 'STEP_0x2', 'WAIT_FOR_SIGNAL']
]);
runInstance2(interpreter.serializeSnapshot());
expect(interpreter.isRunning()).toBe(true);
expect(interpreter.tryStop()).toBe(true);
}
});
interpreter.onDone(() => {
const snapshot = interpreter.getSnapshot();
const path = snapshot.tryGetStatePath();
expect(path).toStrictEqual(['MAIN', 'STEP_0x2', 'WAIT_FOR_SIGNAL']);
isIsInstance1Stopped = true;
});
interpreter.start();
}

runInstance1();
});
});
4 changes: 3 additions & 1 deletion machine/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EventObject, Interpreter, StateMachine, StateNodeConfig, StateSchema, Typestate } from 'xstate';
import { EventObject, Interpreter, StateConfig, StateMachine, StateNodeConfig, StateSchema, Typestate } from 'xstate';
import { SequenceNodeBuilder } from './core/sequence-node-builder';
import { Definition, Step } from 'sequential-workflow-model';
import { MachineUnhandledError } from './machine-unhandled-error';
Expand Down Expand Up @@ -57,3 +57,5 @@ export type SequentialStateMachineInterpreter<TGlobalState> = Interpreter<
>;

export type SignalPayload = Record<string, unknown>;

export type SerializedWorkflowMachineSnapshot<TGlobalState> = StateConfig<MachineContext<TGlobalState>, EventObject>;
3 changes: 3 additions & 0 deletions machine/src/workflow-machine-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { WorkflowMachine } from './workflow-machine';
import { getStepNodeId } from './core';

export interface BuildConfig {
/**
* @deprecated This property will be removed in the next minor version.
*/
initialStepId?: string;
}

Expand Down
15 changes: 11 additions & 4 deletions machine/src/workflow-machine-interpreter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { InterpreterStatus } from 'xstate';
import { SequentialStateMachineInterpreter, SignalPayload } from './types';
import { InterpreterStatus, State } from 'xstate';
import { MachineContext, SequentialStateMachineInterpreter, SerializedWorkflowMachineSnapshot, SignalPayload } from './types';
import { WorkflowMachineSnapshot } from './workflow-machine-snapshot';

export class WorkflowMachineInterpreter<GlobalState> {
public constructor(private readonly interpreter: SequentialStateMachineInterpreter<GlobalState>) {}
public constructor(
private readonly interpreter: SequentialStateMachineInterpreter<GlobalState>,
private readonly initState: State<MachineContext<GlobalState>> | undefined
) {}

public start(): this {
this.interpreter.start();
this.interpreter.start(this.initState);
return this;
}

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

public serializeSnapshot(): SerializedWorkflowMachineSnapshot<GlobalState> {
return this.interpreter.getSnapshot().toJSON() as unknown as SerializedWorkflowMachineSnapshot<GlobalState>;
}

public onDone(callback: () => void): this {
this.interpreter.onStop(callback);
return this;
Expand Down
13 changes: 10 additions & 3 deletions machine/src/workflow-machine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Definition } from 'sequential-workflow-model';
import { interpret } from 'xstate';
import { GlobalStateInitializer, MachineContext, SequentialStateMachine } from './types';
import { interpret, State } from 'xstate';
import { GlobalStateInitializer, MachineContext, SequentialStateMachine, SerializedWorkflowMachineSnapshot } from './types';
import { WorkflowMachineInterpreter } from './workflow-machine-interpreter';

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

public restore(context: MachineContext<GlobalState>): WorkflowMachineInterpreter<GlobalState> {
const machine = this.machine.withContext(context);
return new WorkflowMachineInterpreter(interpret(machine));
return new WorkflowMachineInterpreter(interpret(machine), undefined);
}

public deserializeSnapshot(
serializedSnapshot: SerializedWorkflowMachineSnapshot<GlobalState>
): WorkflowMachineInterpreter<GlobalState> {
const initState = this.machine.resolveState(State.create(serializedSnapshot));
return new WorkflowMachineInterpreter(interpret(this.machine), initState as unknown as State<MachineContext<GlobalState>>);
}

public getNative(): SequentialStateMachine<GlobalState> {
Expand Down
Loading