diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0e037..71b0b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.2.0 + +**Breaking Changes** + +The activity state initializer has one new argument. The order of arguments is not backward compatible. The first argument is a step, the second argument is a global state. + ## 0.1.3 Added two new activities: `LoopActivity` and `BreakActivity`. diff --git a/machine/package.json b/machine/package.json index 6a482cc..e73b8f5 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.1.3", + "version": "0.2.0", "type": "module", "main": "./lib/esm/index.js", "types": "./lib/index.d.ts", @@ -46,6 +46,10 @@ "sequential-workflow-model": "^0.1.1", "xstate": "^4.37.2" }, + "dependencies": { + "sequential-workflow-model": "^0.1.1", + "xstate": "^4.37.2" + }, "devDependencies": { "@types/jest": "^29.4.0", "@typescript-eslint/eslint-plugin": "^5.54.0", @@ -58,9 +62,7 @@ "rollup": "^3.18.0", "rollup-plugin-dts": "^5.2.0", "rollup-plugin-typescript2": "^0.34.1", - "@rollup/plugin-node-resolve": "^15.0.1", - "sequential-workflow-model": "^0.1.1", - "xstate": "^4.37.2" + "@rollup/plugin-node-resolve": "^15.0.1" }, "keywords": [ "workflow", diff --git a/machine/src/activities/atom-activity/atom-activity-node-builder.ts b/machine/src/activities/atom-activity/atom-activity-node-builder.ts index d082832..e44ba57 100644 --- a/machine/src/activities/atom-activity/atom-activity-node-builder.ts +++ b/machine/src/activities/atom-activity/atom-activity-node-builder.ts @@ -1,24 +1,23 @@ import { ActivityNodeBuilder, MachineContext, ActivityNodeConfig, STATE_FAILED_TARGET, STATE_INTERRUPTED_TARGET } from '../../types'; -import { ActivityStateAccessor } from '../../core/activity-context-accessor'; +import { ActivityStateProvider } from '../../core/activity-context-provider'; import { AtomActivityConfig } from './types'; import { catchUnhandledError } from '../../core/catch-unhandled-error'; import { getStepNodeId } from '../../core/safe-node-id'; import { Step } from 'sequential-workflow-model'; import { isInterruptResult } from '../results/interrupt-result'; -export class AtomActivityNodeBuilder implements ActivityNodeBuilder { - public constructor( - private readonly activityStateAccessor: ActivityStateAccessor, - private readonly config: AtomActivityConfig - ) {} +export class AtomActivityNodeBuilder implements ActivityNodeBuilder { + public constructor(private readonly config: AtomActivityConfig) {} - public build(step: TStep, nextNodeTarget: string): ActivityNodeConfig { + public build(step: TStep, nextNodeTarget: string): ActivityNodeConfig { + const activityStateProvider = new ActivityStateProvider(step, this.config.init); const nodeId = getStepNodeId(step.id); + return { id: nodeId, invoke: { - src: catchUnhandledError(async (context: MachineContext) => { - const activityState = this.activityStateAccessor.get(context, nodeId); + src: catchUnhandledError(async (context: MachineContext) => { + const activityState = activityStateProvider.get(context, nodeId); const result = await this.config.handler(step, context.globalState, activityState); if (isInterruptResult(result)) { @@ -29,7 +28,7 @@ export class AtomActivityNodeBuilder) => Boolean(context.interrupted) + cond: (context: MachineContext) => Boolean(context.interrupted) }, { target: nextNodeTarget diff --git a/machine/src/activities/atom-activity/atom-activity.ts b/machine/src/activities/atom-activity/atom-activity.ts index 97dbb83..4ac6c59 100644 --- a/machine/src/activities/atom-activity/atom-activity.ts +++ b/machine/src/activities/atom-activity/atom-activity.ts @@ -1,16 +1,13 @@ import { Activity } from '../../types'; -import { ActivityStateAccessor } from '../../core/activity-context-accessor'; import { AtomActivityNodeBuilder } from './atom-activity-node-builder'; import { AtomActivityConfig } from './types'; import { Step } from 'sequential-workflow-model'; -export function createAtomActivity( - config: AtomActivityConfig -): Activity { - const activityStateAccessor = new ActivityStateAccessor(config.init); - +export function createAtomActivity( + config: AtomActivityConfig +): Activity { return { stepType: config.stepType, - nodeBuilderFactory: () => new AtomActivityNodeBuilder(activityStateAccessor, config) + nodeBuilderFactory: () => new AtomActivityNodeBuilder(config) }; } diff --git a/machine/src/activities/atom-activity/types.ts b/machine/src/activities/atom-activity/types.ts index c653bae..1cd0d84 100644 --- a/machine/src/activities/atom-activity/types.ts +++ b/machine/src/activities/atom-activity/types.ts @@ -2,15 +2,15 @@ import { Step } from 'sequential-workflow-model'; import { ActivityConfig, ActivityStateInitializer } from '../../types'; import { InterruptResult } from '../results/interrupt-result'; -export type AtomActivityHandler = ( +export type AtomActivityHandler = ( step: TStep, - globalState: GlobalState, - activityState: ActivityState + globalState: TGlobalState, + activityState: TActivityState ) => Promise; export type AtomActivityHandlerResult = void | InterruptResult; -export interface AtomActivityConfig extends ActivityConfig { - init: ActivityStateInitializer; - handler: AtomActivityHandler; +export interface AtomActivityConfig extends ActivityConfig { + init: ActivityStateInitializer; + handler: AtomActivityHandler; } diff --git a/machine/src/activities/break-activity/break-activity-node-builder.ts b/machine/src/activities/break-activity/break-activity-node-builder.ts index 7dac00c..7af0dc3 100644 --- a/machine/src/activities/break-activity/break-activity-node-builder.ts +++ b/machine/src/activities/break-activity/break-activity-node-builder.ts @@ -9,17 +9,19 @@ import { STATE_FAILED_TARGET, STATE_INTERRUPTED_TARGET } from '../../types'; -import { ActivityStateAccessor, catchUnhandledError, getStepNodeId } from '../../core'; +import { ActivityStateProvider, catchUnhandledError, getStepNodeId } from '../../core'; import { isInterruptResult } from '../results'; import { isBreakResult } from './break-result'; -export class BreakActivityNodeBuilder implements ActivityNodeBuilder { - public constructor( - private readonly activityStateAccessor: ActivityStateAccessor>, - private readonly config: BreakActivityConfig - ) {} +export class BreakActivityNodeBuilder implements ActivityNodeBuilder { + public constructor(private readonly config: BreakActivityConfig) {} - public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig { + public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig { + const activityStateProvider = new ActivityStateProvider>(step, (s, g) => { + return { + activityState: this.config.init(s, g) + }; + }); const nodeId = getStepNodeId(step.id); const loopName = this.config.loopName(step); @@ -28,8 +30,8 @@ export class BreakActivityNodeBuilder) => { - const internalState = this.activityStateAccessor.get(context, nodeId); + src: catchUnhandledError(async (context: MachineContext) => { + const internalState = activityStateProvider.get(context, nodeId); const result = await this.config.handler(step, context.globalState, internalState.activityState); if (isInterruptResult(result)) { @@ -42,12 +44,12 @@ export class BreakActivityNodeBuilder) => Boolean(context.interrupted) + cond: (context: MachineContext) => Boolean(context.interrupted) }, { target: leaveNodeTarget, - cond: (context: MachineContext) => { - const internalState = this.activityStateAccessor.get(context, nodeId); + cond: (context: MachineContext) => { + const internalState = activityStateProvider.get(context, nodeId); return Boolean(internalState.break); } }, diff --git a/machine/src/activities/break-activity/break-activity.ts b/machine/src/activities/break-activity/break-activity.ts index a958638..20c93e7 100644 --- a/machine/src/activities/break-activity/break-activity.ts +++ b/machine/src/activities/break-activity/break-activity.ts @@ -1,18 +1,13 @@ -import { BreakActivityConfig, BreakActivityState } from './types'; +import { BreakActivityConfig } from './types'; import { BreakActivityNodeBuilder } from './break-activity-node-builder'; import { Step } from 'sequential-workflow-model'; import { Activity } from '../../types'; -import { ActivityStateAccessor } from '../../core'; export function createBreakActivity( config: BreakActivityConfig ): Activity { - const activityStateAccessor = new ActivityStateAccessor>(globalState => ({ - activityState: config.init(globalState) - })); - return { stepType: config.stepType, - nodeBuilderFactory: () => new BreakActivityNodeBuilder(activityStateAccessor, config) + nodeBuilderFactory: () => new BreakActivityNodeBuilder(config) }; } diff --git a/machine/src/activities/break-activity/types.ts b/machine/src/activities/break-activity/types.ts index bd9a734..b1386a6 100644 --- a/machine/src/activities/break-activity/types.ts +++ b/machine/src/activities/break-activity/types.ts @@ -3,21 +3,21 @@ import { ActivityConfig, ActivityStateInitializer } from '../../types'; import { InterruptResult } from '../results'; import { BreakResult } from './break-result'; -export type BreakActivityHandler = ( +export type BreakActivityHandler = ( step: TStep, - globalState: GlobalState, - activityState: ActivityState + globalState: TGlobalState, + activityState: TActivityState ) => Promise; export type BreakActivityHandlerResult = void | InterruptResult | BreakResult; -export interface BreakActivityConfig extends ActivityConfig { +export interface BreakActivityConfig extends ActivityConfig { loopName: (step: TStep) => string; - init: ActivityStateInitializer; - handler: BreakActivityHandler; + init: ActivityStateInitializer; + handler: BreakActivityHandler; } -export interface BreakActivityState { +export interface BreakActivityState { break?: boolean; - activityState: ActivityState; + activityState: TActivityState; } diff --git a/machine/src/activities/container-activity/container-activity-node-builder.ts b/machine/src/activities/container-activity/container-activity-node-builder.ts index 53eb251..75e5b99 100644 --- a/machine/src/activities/container-activity/container-activity-node-builder.ts +++ b/machine/src/activities/container-activity/container-activity-node-builder.ts @@ -6,7 +6,7 @@ import { STATE_INTERRUPTED_TARGET, BuildingContext } from '../../types'; -import { ActivityStateAccessor } from '../../core/activity-context-accessor'; +import { ActivityStateProvider } from '../../core/activity-context-provider'; import { catchUnhandledError } from '../../core/catch-unhandled-error'; import { getStepNodeId } from '../../core/safe-node-id'; import { SequentialStep } from 'sequential-workflow-model'; @@ -14,16 +14,16 @@ import { ContainerActivityConfig, ContainerActivityHandler } from './types'; import { SequenceNodeBuilder } from '../../core'; import { isInterruptResult } from '../results/interrupt-result'; -export class ContainerActivityNodeBuilder - implements ActivityNodeBuilder +export class ContainerActivityNodeBuilder + implements ActivityNodeBuilder { public constructor( - private readonly sequenceNodeBuilder: SequenceNodeBuilder, - private readonly activityStateAccessor: ActivityStateAccessor, - private readonly config: ContainerActivityConfig + private readonly sequenceNodeBuilder: SequenceNodeBuilder, + private readonly config: ContainerActivityConfig ) {} - public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig { + public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig { + const activityStateProvider = new ActivityStateProvider(step, this.config.init); const nodeId = getStepNodeId(step.id); const enterNodeId = `ENTER.${nodeId}`; @@ -32,15 +32,15 @@ export class ContainerActivityNodeBuilder | undefined, + handle: ContainerActivityHandler | undefined, nextStateNodeTarget: string ) => { return { id, invoke: { - src: catchUnhandledError(async (context: MachineContext) => { + src: catchUnhandledError(async (context: MachineContext) => { if (handle) { - const activityState = this.activityStateAccessor.get(context, nodeId); + const activityState = activityStateProvider.get(context, nodeId); const result = await handle(step, context.globalState, activityState); if (isInterruptResult(result)) { @@ -52,7 +52,7 @@ export class ContainerActivityNodeBuilder) => Boolean(context.interrupted) + cond: (context: MachineContext) => Boolean(context.interrupted) }, { target: nextStateNodeTarget diff --git a/machine/src/activities/container-activity/container-activity.ts b/machine/src/activities/container-activity/container-activity.ts index 85f4271..a93396d 100644 --- a/machine/src/activities/container-activity/container-activity.ts +++ b/machine/src/activities/container-activity/container-activity.ts @@ -1,16 +1,13 @@ import { Activity } from '../../types'; -import { ActivityStateAccessor } from '../../core/activity-context-accessor'; import { SequentialStep } from 'sequential-workflow-model'; import { ContainerActivityConfig } from './types'; import { ContainerActivityNodeBuilder } from './container-activity-node-builder'; -export function createContainerActivity( - config: ContainerActivityConfig -): Activity { - const activityStateAccessor = new ActivityStateAccessor(config.init); - +export function createContainerActivity( + config: ContainerActivityConfig +): Activity { return { stepType: config.stepType, - nodeBuilderFactory: sequenceNodeBuilder => new ContainerActivityNodeBuilder(sequenceNodeBuilder, activityStateAccessor, config) + nodeBuilderFactory: sequenceNodeBuilder => new ContainerActivityNodeBuilder(sequenceNodeBuilder, config) }; } diff --git a/machine/src/activities/container-activity/types.ts b/machine/src/activities/container-activity/types.ts index ef20351..3be9a12 100644 --- a/machine/src/activities/container-activity/types.ts +++ b/machine/src/activities/container-activity/types.ts @@ -2,16 +2,16 @@ import { Step } from 'sequential-workflow-model'; import { ActivityConfig, ActivityStateInitializer } from '../../types'; import { InterruptResult } from '../results/interrupt-result'; -export type ContainerActivityHandler = ( +export type ContainerActivityHandler = ( step: TStep, - globalState: GlobalState, - activityState: ActivityState + globalState: TGlobalState, + activityState: TActivityState ) => Promise; export type ContainerActivityHandlerResult = void | InterruptResult; -export interface ContainerActivityConfig extends ActivityConfig { - init: ActivityStateInitializer; - onEnter?: ContainerActivityHandler; - onLeave?: ContainerActivityHandler; +export interface ContainerActivityConfig extends ActivityConfig { + init: ActivityStateInitializer; + onEnter?: ContainerActivityHandler; + onLeave?: ContainerActivityHandler; } diff --git a/machine/src/activities/fork-activity/fork-activity-node-builder.ts b/machine/src/activities/fork-activity/fork-activity-node-builder.ts index 0e54e8b..f2e40a3 100644 --- a/machine/src/activities/fork-activity/fork-activity-node-builder.ts +++ b/machine/src/activities/fork-activity/fork-activity-node-builder.ts @@ -6,7 +6,7 @@ import { STATE_FAILED_TARGET, STATE_INTERRUPTED_TARGET } from '../../types'; -import { ActivityStateAccessor } from '../../core/activity-context-accessor'; +import { ActivityStateProvider } from '../../core/activity-context-provider'; import { ForkActivityConfig, ForkActivityState } from './types'; import { catchUnhandledError } from '../../core/catch-unhandled-error'; import { SequenceNodeBuilder } from '../../core/sequence-node-builder'; @@ -15,27 +15,34 @@ import { BranchedStep } from 'sequential-workflow-model'; import { isBranchNameResult } from '../results/branch-name-result'; import { isInterruptResult } from '../results/interrupt-result'; -export class ForkActivityNodeBuilder implements ActivityNodeBuilder { +export class ForkActivityNodeBuilder + implements ActivityNodeBuilder +{ public constructor( - private readonly sequenceNodeBuilder: SequenceNodeBuilder, - private readonly activityStateAccessor: ActivityStateAccessor>, - private readonly config: ForkActivityConfig + private readonly sequenceNodeBuilder: SequenceNodeBuilder, + private readonly config: ForkActivityConfig ) {} - public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig { + public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig { + const activityStateProvider = new ActivityStateProvider>(step, (s, g) => { + return { + activityState: this.config.init(s, g) + }; + }); + const nodeId = getStepNodeId(step.id); const branchNames = Object.keys(step.branches); - const states: Record> = {}; + const states: Record> = {}; for (const branchName of branchNames) { const branchNodeId = getBranchNodeId(branchName); states[branchNodeId] = this.sequenceNodeBuilder.build(buildingContext, step.branches[branchName], nextNodeTarget); } - const CONDITION: ActivityNodeConfig = { + const CONDITION: ActivityNodeConfig = { invoke: { - src: catchUnhandledError(async (context: MachineContext) => { - const internalState = this.activityStateAccessor.get(context, nodeId); + src: catchUnhandledError(async (context: MachineContext) => { + const internalState = activityStateProvider.get(context, nodeId); const result = await this.config.handler(step, context.globalState, internalState.activityState); if (isInterruptResult(result)) { @@ -56,13 +63,13 @@ export class ForkActivityNodeBuilder) => Boolean(context.interrupted) + cond: (context: MachineContext) => Boolean(context.interrupted) }, ...branchNames.map(branchName => { return { target: getBranchNodeId(branchName), - cond: (context: MachineContext) => { - const activityState = this.activityStateAccessor.get(context, nodeId); + cond: (context: MachineContext) => { + const activityState = activityStateProvider.get(context, nodeId); return activityState.targetBranchName === branchName; } }; diff --git a/machine/src/activities/fork-activity/fork-activity.ts b/machine/src/activities/fork-activity/fork-activity.ts index 00847cc..ffbac62 100644 --- a/machine/src/activities/fork-activity/fork-activity.ts +++ b/machine/src/activities/fork-activity/fork-activity.ts @@ -1,18 +1,13 @@ import { Activity } from '../../types'; -import { ActivityStateAccessor } from '../../core/activity-context-accessor'; import { ForkActivityNodeBuilder } from './fork-activity-node-builder'; -import { ForkActivityConfig, ForkActivityState } from './types'; +import { ForkActivityConfig } from './types'; import { BranchedStep } from 'sequential-workflow-model'; -export function createForkActivity( - config: ForkActivityConfig -): Activity { - const activityStateAccessor = new ActivityStateAccessor>(globalState => ({ - activityState: config.init(globalState) - })); - +export function createForkActivity( + config: ForkActivityConfig +): Activity { return { stepType: config.stepType, - nodeBuilderFactory: sequenceNodeBuilder => new ForkActivityNodeBuilder(sequenceNodeBuilder, activityStateAccessor, config) + nodeBuilderFactory: sequenceNodeBuilder => new ForkActivityNodeBuilder(sequenceNodeBuilder, config) }; } diff --git a/machine/src/activities/fork-activity/types.ts b/machine/src/activities/fork-activity/types.ts index 203834d..7c5196e 100644 --- a/machine/src/activities/fork-activity/types.ts +++ b/machine/src/activities/fork-activity/types.ts @@ -4,18 +4,18 @@ import { InterruptResult } from '../results/interrupt-result'; import { BranchNameResult } from '../results/branch-name-result'; export type ForkActivityHandlerResult = InterruptResult | BranchNameResult; -export type ForkActivityHandler = ( +export type ForkActivityHandler = ( step: TStep, - globalState: GlobalState, - activityState: ActivityState + globalState: TGlobalState, + activityState: TActivityState ) => Promise; -export interface ForkActivityConfig extends ActivityConfig { - init: ActivityStateInitializer; - handler: ForkActivityHandler; +export interface ForkActivityConfig extends ActivityConfig { + init: ActivityStateInitializer; + handler: ForkActivityHandler; } -export interface ForkActivityState { +export interface ForkActivityState { targetBranchName?: string; - activityState: ActivityState; + activityState: TActivityState; } diff --git a/machine/src/activities/interruption-activity/interruption-activity-node-builder.ts b/machine/src/activities/interruption-activity/interruption-activity-node-builder.ts index 9024d23..8e12012 100644 --- a/machine/src/activities/interruption-activity/interruption-activity-node-builder.ts +++ b/machine/src/activities/interruption-activity/interruption-activity-node-builder.ts @@ -4,15 +4,15 @@ import { getStepNodeId } from '../../core/safe-node-id'; import { Step } from 'sequential-workflow-model'; import { InterruptionActivityConfig } from './types'; -export class InterruptionActivityNodeBuilder implements ActivityNodeBuilder { - public constructor(private readonly config: InterruptionActivityConfig) {} +export class InterruptionActivityNodeBuilder implements ActivityNodeBuilder { + public constructor(private readonly config: InterruptionActivityConfig) {} - public build(step: TStep): ActivityNodeConfig { + public build(step: TStep): ActivityNodeConfig { const nodeId = getStepNodeId(step.id); return { id: nodeId, invoke: { - src: catchUnhandledError((context: MachineContext) => this.config.handler(step, context.globalState)), + src: catchUnhandledError((context: MachineContext) => this.config.handler(step, context.globalState)), onDone: [ { target: STATE_INTERRUPTED_TARGET diff --git a/machine/src/activities/interruption-activity/interruption-activity.ts b/machine/src/activities/interruption-activity/interruption-activity.ts index 44c089e..2e3a35b 100644 --- a/machine/src/activities/interruption-activity/interruption-activity.ts +++ b/machine/src/activities/interruption-activity/interruption-activity.ts @@ -3,9 +3,9 @@ import { InterruptionActivityConfig } from './types'; import { Step } from 'sequential-workflow-model'; import { InterruptionActivityNodeBuilder } from './interruption-activity-node-builder'; -export function createInterruptionActivity( - config: InterruptionActivityConfig -): Activity { +export function createInterruptionActivity( + config: InterruptionActivityConfig +): Activity { return { stepType: config.stepType, nodeBuilderFactory: () => new InterruptionActivityNodeBuilder(config) diff --git a/machine/src/activities/interruption-activity/types.ts b/machine/src/activities/interruption-activity/types.ts index c42bf60..915a3b8 100644 --- a/machine/src/activities/interruption-activity/types.ts +++ b/machine/src/activities/interruption-activity/types.ts @@ -1,8 +1,8 @@ import { Step } from 'sequential-workflow-model'; import { ActivityConfig } from '../../types'; -export type InterruptionActivityHandler = (step: TStep, globalState: GlobalState) => Promise; +export type InterruptionActivityHandler = (step: TStep, globalState: TGlobalState) => Promise; -export interface InterruptionActivityConfig extends ActivityConfig { - handler: InterruptionActivityHandler; +export interface InterruptionActivityConfig extends ActivityConfig { + handler: InterruptionActivityHandler; } diff --git a/machine/src/activities/loop-activity/loop-activity-node-builder.ts b/machine/src/activities/loop-activity/loop-activity-node-builder.ts index 4357460..ea7fc88 100644 --- a/machine/src/activities/loop-activity/loop-activity-node-builder.ts +++ b/machine/src/activities/loop-activity/loop-activity-node-builder.ts @@ -1,7 +1,7 @@ import { SequentialStep } from 'sequential-workflow-model'; import { getLoopStack } from '../loop-stack'; import { LoopActivityConfig, LoopActivityState } from './types'; -import { ActivityStateAccessor, SequenceNodeBuilder, catchUnhandledError, getStepNodeId } from '../../core'; +import { ActivityStateProvider, SequenceNodeBuilder, catchUnhandledError, getStepNodeId } from '../../core'; import { ActivityNodeBuilder, ActivityNodeConfig, @@ -12,14 +12,18 @@ import { } from '../../types'; import { isInterruptResult } from '../results'; -export class LoopActivityNodeBuilder implements ActivityNodeBuilder { +export class LoopActivityNodeBuilder + implements ActivityNodeBuilder +{ public constructor( - private readonly sequenceNodeBuilder: SequenceNodeBuilder, - private readonly activityStateAccessor: ActivityStateAccessor>, - private readonly config: LoopActivityConfig + private readonly sequenceNodeBuilder: SequenceNodeBuilder, + private readonly config: LoopActivityConfig ) {} - public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig { + public build(step: TStep, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig { + const activityStateProvider = new ActivityStateProvider>(step, (s, g) => ({ + activityState: this.config.init(s, g) + })); const nodeId = getStepNodeId(step.id); const conditionNodeId = `CONDITION.${nodeId}`; @@ -41,10 +45,10 @@ export class LoopActivityNodeBuilder) => { + src: catchUnhandledError(async (context: MachineContext) => { if (this.config.onEnter) { - const internalContext = this.activityStateAccessor.get(context, nodeId); - this.config.onEnter(step, context.globalState, internalContext.activityState); + const internalState = activityStateProvider.get(context, nodeId); + this.config.onEnter(step, context.globalState, internalState.activityState); } }), onDone: 'CONDITION', @@ -54,8 +58,8 @@ export class LoopActivityNodeBuilder) => { - const internalState = this.activityStateAccessor.get(context, nodeId); + src: catchUnhandledError(async (context: MachineContext) => { + const internalState = activityStateProvider.get(context, nodeId); const result = await this.config.condition(step, context.globalState, internalState.activityState); if (isInterruptResult(result)) { @@ -68,12 +72,12 @@ export class LoopActivityNodeBuilder) => Boolean(context.interrupted) + cond: (context: MachineContext) => Boolean(context.interrupted) }, { target: 'LOOP', - cond: (context: MachineContext) => { - const activityState = this.activityStateAccessor.get(context, nodeId); + cond: (context: MachineContext) => { + const activityState = activityStateProvider.get(context, nodeId); return Boolean(activityState.continue); } }, @@ -88,9 +92,9 @@ export class LoopActivityNodeBuilder) => { + src: catchUnhandledError(async (context: MachineContext) => { if (this.config.onLeave) { - const internalState = this.activityStateAccessor.get(context, nodeId); + const internalState = activityStateProvider.get(context, nodeId); this.config.onLeave(step, context.globalState, internalState.activityState); } }), diff --git a/machine/src/activities/loop-activity/loop-activity.ts b/machine/src/activities/loop-activity/loop-activity.ts index 4905ffa..9406098 100644 --- a/machine/src/activities/loop-activity/loop-activity.ts +++ b/machine/src/activities/loop-activity/loop-activity.ts @@ -1,18 +1,13 @@ -import { ActivityStateAccessor } from '../../core'; import { Activity } from '../../types'; import { LoopActivityNodeBuilder } from './loop-activity-node-builder'; -import { LoopActivityConfig, LoopActivityState } from './types'; +import { LoopActivityConfig } from './types'; import { SequentialStep } from 'sequential-workflow-model'; -export function createLoopActivity( - config: LoopActivityConfig -): Activity { - const activityStateAccessor = new ActivityStateAccessor>((globalState: GlobalState) => ({ - activityState: config.init(globalState) - })); - +export function createLoopActivity( + config: LoopActivityConfig +): Activity { return { stepType: config.stepType, - nodeBuilderFactory: sequenceNodeBuilder => new LoopActivityNodeBuilder(sequenceNodeBuilder, activityStateAccessor, config) + nodeBuilderFactory: sequenceNodeBuilder => new LoopActivityNodeBuilder(sequenceNodeBuilder, config) }; } diff --git a/machine/src/activities/loop-activity/types.ts b/machine/src/activities/loop-activity/types.ts index a8591cb..18da69b 100644 --- a/machine/src/activities/loop-activity/types.ts +++ b/machine/src/activities/loop-activity/types.ts @@ -3,26 +3,26 @@ import { ActivityConfig, ActivityStateInitializer } from '../../types'; import { InterruptResult } from '../results'; export type LoopActivityHandlerResult = boolean | InterruptResult; -export type LoopActivityEventHandler = ( +export type LoopActivityEventHandler = ( step: TStep, - globalState: GlobalState, - activityState: ActivityState + globalState: TGlobalState, + activityState: TActivityState ) => void; -export type LoopActivityConditionHandler = ( +export type LoopActivityConditionHandler = ( step: TStep, - globalState: GlobalState, - activityState: ActivityState + globalState: TGlobalState, + activityState: TActivityState ) => Promise; -export interface LoopActivityConfig extends ActivityConfig { +export interface LoopActivityConfig extends ActivityConfig { loopName: (step: TStep) => string; - init: ActivityStateInitializer; - condition: LoopActivityConditionHandler; - onEnter?: LoopActivityEventHandler; - onLeave?: LoopActivityEventHandler; + init: ActivityStateInitializer; + condition: LoopActivityConditionHandler; + onEnter?: LoopActivityEventHandler; + onLeave?: LoopActivityEventHandler; } -export interface LoopActivityState { +export interface LoopActivityState { continue?: boolean; - activityState: ActivityState; + activityState: TActivityState; } diff --git a/machine/src/core/activity-context-accessor.spec.ts b/machine/src/core/activity-context-accessor.spec.ts deleted file mode 100644 index b79c00a..0000000 --- a/machine/src/core/activity-context-accessor.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MachineContext } from '../types'; -import { ActivityStateAccessor } from './activity-context-accessor'; - -describe('ActivityStateAccessor()', () => { - it('returns the same instance for the same id', () => { - const machineContext: MachineContext = { - activityStates: {}, - globalState: {} - }; - - const accessor = new ActivityStateAccessor(() => ({ - value: Math.random() - })); - - const instance1 = accessor.get(machineContext, '0x00001'); - const instance2 = accessor.get(machineContext, '0x00001'); - - expect(instance1).toBe(instance2); - expect(instance1.value).toBe(instance2.value); - }); -}); diff --git a/machine/src/core/activity-context-accessor.ts b/machine/src/core/activity-context-accessor.ts deleted file mode 100644 index 0ff11b8..0000000 --- a/machine/src/core/activity-context-accessor.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ActivityStateInitializer, MachineContext } from '../types'; - -export class ActivityStateAccessor { - public constructor(private readonly init: ActivityStateInitializer) {} - - public get(context: MachineContext, nodeId: string): ActivityState { - let activityState = context.activityStates[nodeId] as ActivityState | undefined; - if (!activityState) { - activityState = this.init(context.globalState); - context.activityStates[nodeId] = activityState; - } - return activityState; - } -} diff --git a/machine/src/core/activity-context-provider.spec.ts b/machine/src/core/activity-context-provider.spec.ts new file mode 100644 index 0000000..ae02e93 --- /dev/null +++ b/machine/src/core/activity-context-provider.spec.ts @@ -0,0 +1,33 @@ +import { Step } from 'sequential-workflow-model'; +import { MachineContext } from '../types'; +import { ActivityStateProvider } from './activity-context-provider'; + +describe('ActivityStateProvider()', () => { + it('returns the same instance for the same id', () => { + const machineContext: MachineContext = { + activityStates: {}, + globalState: {} + }; + const step: Step = { + id: '0x1', + componentType: 'task', + type: 'test', + name: 'Test', + properties: {} + }; + const nodeId = step.id; + + const provider = new ActivityStateProvider(step, s => { + expect(s).toBe(step); + return { + value: Math.random() + }; + }); + + const instance1 = provider.get(machineContext, nodeId); + const instance2 = provider.get(machineContext, nodeId); + + expect(instance1).toBe(instance2); + expect(instance1.value).toBe(instance2.value); + }); +}); diff --git a/machine/src/core/activity-context-provider.ts b/machine/src/core/activity-context-provider.ts new file mode 100644 index 0000000..46ba604 --- /dev/null +++ b/machine/src/core/activity-context-provider.ts @@ -0,0 +1,18 @@ +import { Step } from 'sequential-workflow-model'; +import { ActivityStateInitializer, MachineContext } from '../types'; + +export class ActivityStateProvider { + public constructor( + private readonly step: TStep, + private readonly init: ActivityStateInitializer + ) {} + + public get(context: MachineContext, nodeId: string): TActivityState { + let activityState = context.activityStates[nodeId] as TActivityState | undefined; + if (!activityState) { + activityState = this.init(this.step, context.globalState); + context.activityStates[nodeId] = activityState; + } + return activityState; + } +} diff --git a/machine/src/core/activity-node-builder-resolver.ts b/machine/src/core/activity-node-builder-resolver.ts index 8574698..ad3a771 100644 --- a/machine/src/core/activity-node-builder-resolver.ts +++ b/machine/src/core/activity-node-builder-resolver.ts @@ -2,15 +2,15 @@ import { ActivitySet } from './activity-set'; import { ActivityNodeBuilder } from '../types'; import { SequenceNodeBuilder } from './sequence-node-builder'; -export class ActivityNodeBuilderResolver { - private readonly builders: Map> = new Map(); +export class ActivityNodeBuilderResolver { + private readonly builders: Map> = new Map(); public constructor( - private readonly activitiesSet: ActivitySet, - private readonly sequenceNodeBuilder: SequenceNodeBuilder + private readonly activitiesSet: ActivitySet, + private readonly sequenceNodeBuilder: SequenceNodeBuilder ) {} - public resolve(stepType: string): ActivityNodeBuilder { + public resolve(stepType: string): ActivityNodeBuilder { let builder = this.builders.get(stepType); if (builder) { return builder; diff --git a/machine/src/core/activity-set.ts b/machine/src/core/activity-set.ts index 0b7ab87..f6cf023 100644 --- a/machine/src/core/activity-set.ts +++ b/machine/src/core/activity-set.ts @@ -1,9 +1,9 @@ import { Activity } from '../types'; -export class ActivitySet { - public constructor(private readonly activities: Map>) {} +export class ActivitySet { + public constructor(private readonly activities: Map>) {} - public get(stepType: string): Activity { + public get(stepType: string): Activity { const provider = this.activities.get(stepType); if (!provider) { throw new Error(`Cannot find activity for step type: ${stepType}`); @@ -12,8 +12,8 @@ export class ActivitySet { } } -export function createActivitySet(activities: Activity[]): ActivitySet { - const map = new Map>(); +export function createActivitySet(activities: Activity[]): ActivitySet { + const map = new Map>(); for (const activity of activities) { if (map.has(activity.stepType)) { throw new Error(`Duplicate step type: ${activity.stepType}`); diff --git a/machine/src/core/catch-unhandled-error.ts b/machine/src/core/catch-unhandled-error.ts index 023412b..6ad26f0 100644 --- a/machine/src/core/catch-unhandled-error.ts +++ b/machine/src/core/catch-unhandled-error.ts @@ -1,8 +1,8 @@ import { EventObject } from 'xstate'; import { MachineContext } from '../types'; -export function catchUnhandledError(callback: (context: MachineContext, event: EventObject) => Promise) { - return async (context: MachineContext, event: EventObject) => { +export function catchUnhandledError(callback: (context: MachineContext, event: EventObject) => Promise) { + return async (context: MachineContext, event: EventObject) => { try { await callback(context, event); } catch (e) { diff --git a/machine/src/core/index.ts b/machine/src/core/index.ts index 21317db..8223718 100644 --- a/machine/src/core/index.ts +++ b/machine/src/core/index.ts @@ -1,5 +1,5 @@ export * from './activity-set'; -export * from './activity-context-accessor'; +export * from './activity-context-provider'; export * from './activity-node-builder-resolver'; export * from './catch-unhandled-error'; export * from './safe-node-id'; diff --git a/machine/src/core/sequence-node-builder.ts b/machine/src/core/sequence-node-builder.ts index d9a9142..8824014 100644 --- a/machine/src/core/sequence-node-builder.ts +++ b/machine/src/core/sequence-node-builder.ts @@ -4,12 +4,12 @@ import { ActivityNodeBuilderResolver } from './activity-node-builder-resolver'; import { getStepNodeId } from './safe-node-id'; import { Sequence } from 'sequential-workflow-model'; -export class SequenceNodeBuilder { - private readonly resolver = new ActivityNodeBuilderResolver(this.activitySet, this); +export class SequenceNodeBuilder { + private readonly resolver = new ActivityNodeBuilderResolver(this.activitySet, this); - public constructor(private readonly activitySet: ActivitySet) {} + public constructor(private readonly activitySet: ActivitySet) {} - public build(buildingContext: BuildingContext, sequence: Sequence, nextNodeTarget: string): ActivityNodeConfig { + public build(buildingContext: BuildingContext, sequence: Sequence, nextNodeTarget: string): ActivityNodeConfig { if (sequence.length === 0) { return { invoke: { @@ -19,7 +19,7 @@ export class SequenceNodeBuilder { }; } - const states: Record> = {}; + const states: Record> = {}; for (let index = 0; index < sequence.length; index++) { const step = sequence[index]; const nodeId = getStepNodeId(step.id); diff --git a/machine/src/core/serialize-state-value.spec.ts b/machine/src/core/state-path-reader.spec.ts similarity index 60% rename from machine/src/core/serialize-state-value.spec.ts rename to machine/src/core/state-path-reader.spec.ts index ce287d5..67cedff 100644 --- a/machine/src/core/serialize-state-value.spec.ts +++ b/machine/src/core/state-path-reader.spec.ts @@ -1,13 +1,13 @@ -import { getStatePath } from './serialize-state-value'; +import { readStatePath } from './state-path-reader'; -describe('serializeStateValue()', () => { +describe('readStatePath()', () => { it('returns correct value for string', () => { - expect(getStatePath('MAIN')[0]).toBe('MAIN'); + expect(readStatePath('MAIN')[0]).toBe('MAIN'); }); it('returns correct value for object', () => { const value = { MAIN: { _0x1: 'WAIT_FOR_SIGNAL' } }; - const path = getStatePath(value); + const path = readStatePath(value); expect(path[0]).toBe('MAIN'); expect(path[1]).toBe('_0x1'); expect(path[2]).toBe('WAIT_FOR_SIGNAL'); diff --git a/machine/src/core/serialize-state-value.ts b/machine/src/core/state-path-reader.ts similarity index 86% rename from machine/src/core/serialize-state-value.ts rename to machine/src/core/state-path-reader.ts index 9f68f27..e3ebbbc 100644 --- a/machine/src/core/serialize-state-value.ts +++ b/machine/src/core/state-path-reader.ts @@ -1,6 +1,6 @@ import { StateValue } from 'xstate'; -export function getStatePath(stateValue: StateValue): string[] { +export function readStatePath(stateValue: StateValue): string[] { if (typeof stateValue === 'string') { return [stateValue]; } diff --git a/machine/src/types.ts b/machine/src/types.ts index decec37..55d8620 100644 --- a/machine/src/types.ts +++ b/machine/src/types.ts @@ -13,47 +13,50 @@ export const STATE_FAILED_TARGET = `#${STATE_FAILED_ID}`; export type MachineUnhandledError = unknown; -export interface MachineContext { +export interface MachineContext { interrupted?: string; unhandledError?: MachineUnhandledError; - globalState: GlobalState; + globalState: TGlobalState; activityStates: Record; } -export interface Activity { +export interface Activity { stepType: string; - nodeBuilderFactory: ActivityNodeBuilderFactory; + nodeBuilderFactory: ActivityNodeBuilderFactory; } export interface ActivityConfig { stepType: TStep['type']; } -export type ActivityNodeBuilderFactory = ( - sequenceNodeBuilder: SequenceNodeBuilder -) => ActivityNodeBuilder; +export type ActivityNodeBuilderFactory = ( + sequenceNodeBuilder: SequenceNodeBuilder +) => ActivityNodeBuilder; export interface BuildingContext { [name: string]: unknown; } -export interface ActivityNodeBuilder { - build(step: Step, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig; +export interface ActivityNodeBuilder { + build(step: Step, nextNodeTarget: string, buildingContext: BuildingContext): ActivityNodeConfig; } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ActivityNodeConfig = StateNodeConfig, Record, EventObject>; +export type ActivityNodeConfig = StateNodeConfig, Record, EventObject>; -export type GlobalStateInitializer = (definition: Definition) => GlobalState; +export type GlobalStateInitializer = (definition: Definition) => TGlobalState; -export type ActivityStateInitializer = (globalState: GlobalState) => ActivityState; +export type ActivityStateInitializer = ( + step: TStep, + globalState: TGlobalState +) => TActivityState; -export type SequentialStateMachine = StateMachine, StateSchema, EventObject>; -export type SequentialStateMachineInterpreter = Interpreter< - MachineContext, +export type SequentialStateMachine = StateMachine, StateSchema, EventObject>; +export type SequentialStateMachineInterpreter = Interpreter< + MachineContext, StateSchema, EventObject, - Typestate>, + Typestate>, // eslint-disable-next-line @typescript-eslint/no-explicit-any any >; diff --git a/machine/src/workflow-machine-interpreter.ts b/machine/src/workflow-machine-interpreter.ts index 60681b6..72870ef 100644 --- a/machine/src/workflow-machine-interpreter.ts +++ b/machine/src/workflow-machine-interpreter.ts @@ -1,4 +1,4 @@ -import { getStatePath } from './core/serialize-state-value'; +import { readStatePath } from './core/state-path-reader'; import { MachineUnhandledError, SequentialStateMachineInterpreter } from './types'; export class WorkflowMachineInterpreter { @@ -14,7 +14,7 @@ export class WorkflowMachineInterpreter { return { globalState: snapshot.context.globalState, unhandledError: snapshot.context.unhandledError, - statePath: getStatePath(snapshot.value) + statePath: readStatePath(snapshot.value) }; }