diff --git a/CHANGELOG.md b/CHANGELOG.md index 05df1ca..38c4027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/machine/package.json b/machine/package.json index 4befeaa..4936919 100644 --- a/machine/package.json +++ b/machine/package.json @@ -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", diff --git a/machine/src/activities/signal-activity/signal-activity-persistence.spec.ts b/machine/src/activities/signal-activity/signal-activity-persistence.spec.ts new file mode 100755 index 0000000..bfe4e77 --- /dev/null +++ b/machine/src/activities/signal-activity/signal-activity-persistence.spec.ts @@ -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([ + createAtomActivityFromHandler('ping', async (step, globalState) => { + globalState.logs.push(`ping:${step.name}`); + }), + createSignalActivity('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) { + 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(); + }); +}); diff --git a/machine/src/types.ts b/machine/src/types.ts index d6109b1..3b2fa33 100644 --- a/machine/src/types.ts +++ b/machine/src/types.ts @@ -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'; @@ -57,3 +57,5 @@ export type SequentialStateMachineInterpreter = Interpreter< >; export type SignalPayload = Record; + +export type SerializedWorkflowMachineSnapshot = StateConfig, EventObject>; diff --git a/machine/src/workflow-machine-builder.ts b/machine/src/workflow-machine-builder.ts index 8a21ef3..b5d08cf 100644 --- a/machine/src/workflow-machine-builder.ts +++ b/machine/src/workflow-machine-builder.ts @@ -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; } diff --git a/machine/src/workflow-machine-interpreter.ts b/machine/src/workflow-machine-interpreter.ts index 62b37d2..32dad4c 100644 --- a/machine/src/workflow-machine-interpreter.ts +++ b/machine/src/workflow-machine-interpreter.ts @@ -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 { - public constructor(private readonly interpreter: SequentialStateMachineInterpreter) {} + public constructor( + private readonly interpreter: SequentialStateMachineInterpreter, + private readonly initState: State> | undefined + ) {} public start(): this { - this.interpreter.start(); + this.interpreter.start(this.initState); return this; } @@ -15,6 +18,10 @@ export class WorkflowMachineInterpreter { return new WorkflowMachineSnapshot(snapshot.context.globalState, snapshot.context.unhandledError, snapshot.value); } + public serializeSnapshot(): SerializedWorkflowMachineSnapshot { + return this.interpreter.getSnapshot().toJSON() as unknown as SerializedWorkflowMachineSnapshot; + } + public onDone(callback: () => void): this { this.interpreter.onStop(callback); return this; diff --git a/machine/src/workflow-machine.ts b/machine/src/workflow-machine.ts index ff2ba61..d06e7bb 100644 --- a/machine/src/workflow-machine.ts +++ b/machine/src/workflow-machine.ts @@ -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 { @@ -22,7 +22,14 @@ export class WorkflowMachine { public restore(context: MachineContext): WorkflowMachineInterpreter { const machine = this.machine.withContext(context); - return new WorkflowMachineInterpreter(interpret(machine)); + return new WorkflowMachineInterpreter(interpret(machine), undefined); + } + + public deserializeSnapshot( + serializedSnapshot: SerializedWorkflowMachineSnapshot + ): WorkflowMachineInterpreter { + const initState = this.machine.resolveState(State.create(serializedSnapshot)); + return new WorkflowMachineInterpreter(interpret(this.machine), initState as unknown as State>); } public getNative(): SequentialStateMachine {