Skip to content

拦截状态

flexstate支持定义状态转换钩子(Hook),允许对转换过程进行拦截。

支持的钩子类型

状态转换钩子指的是在状态转换其间调用的函数,支持以下类型的拦截钩子。

  • enter:当准备进入某个状态时调用,可以通过返回false触发错误来阻止状态转换。
  • leave:当准备离开某个状态时调用,可以通过返回false触发错误来阻止状态转换。
  • done:当已转换到某个状态时调用,该钩子不处理错误,不能通过返回false触发错误来阻止状态转换。
  • resume:当执行enter钩子出错时,会调用上一个状态的resume来尝试恢复和消除leave产生的副作用。

实现原理

状态转换钩子是在调用transition方法转换状态时被调用的函数。transition方法并不是直接调用这些定义的钩子函数的,而是通过emitAsync来触发事件转换事件,然后钩子函数订阅事件。假设当前状态是A,当调用transition("B")方法时将发生:

  • 先调用canTransition("A","B")方法来判断能否从A转换到B状态,如果canTransition返回false,则代表不允许从A转换到B状态,将触发TransitionError错误。
  • 接下来会触发emitAsync("A/leave")事件,代表将离开A状态。所有leave钩子函数本质上均是订阅了A/leave事件,并且钩子函数可以通过返回false触发错误来阻止离开A状态。emitAsync实质上是通过Promise.all调用所有钩子函数的。
  • 如果所有订阅了A/leave事件的钩子函数均没有阻止离开A状态。接下来,应会触发emitAsync("B/enter")事件,所有leave钩子函数同样是订阅了B/enter事件,可以在进入B状态前做一些事件,也可以通过返回false触发错误来阻止进入B状态
  • 如果成功进入B状态,则会emitAsync("B/done")事件。如果B/enter返回false或者触发错误而导致无法进入B状态,而会触发emitAsync("A/resume")

可以看出,状态机会在转换过程中,视情况触发<State>/leave<State>/enter<State>/resume<State>/done等事件。因此,开发者可以通过fsm.on(<State>/leave,callback)fsm.on(<State>/enter,callback)fsm.on(<State>/resume,callback)fsm.on(<State>/done,callback)来订阅转换钩子。

定义钩子

可以通过以下方法来声明拦截钩子,拦截钩子可以是同步函数,也可以是异步函数。

钩子函数签名如下:

typescript
export interface FlexStateTransitionEventArguments{
    event? : 'CANCEL' | 'BEGIN' | 'END' | 'ERROR'
    from  : string
    to    : string
    error?: Error
    params?:any
    [key: string]:any
}

// 钩子参数 {from,to,error,params,retry,retryCount}
export type FlexStateTransitionHookArguments = Exclude<FlexStateTransitionEventArguments,'event'> & {
    retryCount    : number                                                  // 重试次数,
    retry         : Function | ((interval?:number)=>void)                   // 马上执行重试
}
type FlexStateTransitionHook = ((args:FlexStateTransitionHookArguments)=>Awaited<Promise<any>> | void ) | undefined  
type FlexStateTransitionHookExt = FlexStateTransitionHook | [FlexStateTransitionHook,{timeout:number}]

定义钩子函数有以下几种方法:

构造时传入

typescript
new FlexStateMachine({ 
    states:{
        Initial:{           
            enter:async function({from,to,error,params,retry,retryCount}){},
            leave:async ({from,to,error,params,retry,retryCount})=>{},
            done:async ({from,to,error,params})=>{},
            resume:async ({from,to,error,params,retry,retryCount})=>{},
        },
    }
    //...    
}

定义类方法进行侦听

context中直接定义on<State>Enter/on<State>Levae/on<State>Done/on<State>/on<State>Resume实例方法,如:

typescript

class MyStateMachine extends FlexStateMachine{
    async onInitialEnter({from,to,error,params,retry,retryCount}){ }
    async onInitialLeave({from,to,error,params,retry,retryCount}){ }
    async onInitialDone({from,to,error,params}){ }
    async onInitialResume({from,to,error}){ }
    async onInitial({from,to,error,params}){ }
  
    async onConnectingEnter({from,to,error,params,retry,retryCount}){...}
    async onConnectingLeave({from,to,error,params,retry,retryCount}){...}
    async onConnectingResume({from,to,error,params,retry,retryCount}){ }
    async onConnectingDone({from,to,error,params})
    async onConnecting({from,to,error,params})
                                       
    async onConnectedEnter({from,to,error,params,retry,retryCount}){...}
    async onConnectedLeave({from,to,error,params,retry,retryCount}){...}
    async onConnectedResume({from,to,error,params,retry,retryCount}){...}
    async onConnected({from,to,error,params}){...}
    async onConnectedDone({from,to,error,params}){...}
    ......
}

注意:

  • 在类中声明的钩子方法名称会使用状态名称首字母大写形式
  • on<State>on<State>Done的别名,如上例中,onConnected===onConnectedDone。一般只需要声明一个即可。

在状态中直接订阅事件

typescript
fsm.states.Connected.on("enter",({from,to,error,params,retry,retryCount})=>{ })
fsm.states.Connected.on("leave",({from,to,error,params,retry,retryCount})=>{ })
fsm.states.Connected.on("done",({from,to,error,params})=>{ })
fsm.states.Connected.on("resume",({from,to,error})=>{ })

订阅状态转换事件

typescript
fsm.on("Connected/enter",({from,to,error,params,retry,retryCount})=>{})
fsm.on("Connected/leave",({from,to,error,params,retry,retryCount})=>{ })
fsm.on("Connected/done",({from,to,params})=>{ })
fsm.on("Connected/resume",({from,to,error,params,retry,retryCount})=>{ })

钩子事件参数

钩子函数的参数如下:

  • from: string:上一个状态
  • to: string:下一个状态
  • params:any:转换时传入的参数
  • error: Error | undefined: 发生错误时的错误
  • retryCountretry:用于进行重试的参数,详见错误重试说明。

阻止转换过程

状态转换除了受state.next约束外,还可以通过状态转换钩子来拦截约束。状态转换钩子函数在状态转换其间调用,可以对转换行为进行拦截并作出相应的拦截处理

  • 通过返回false来明确阻止转换过程。
typescript
class MyStateMachine extends FlexStateMachine{
    async onInitialLeave({from,to,error,params,retry,retryCount}){
       // ...
       return false           // 返回false代表不充许离开Initial状态。
    } 
}
  • 通过触发错误来阻止转换过程
typescript
class MyStateMachine extends FlexStateMachine{
    async onInitialLeave({from,to,error,params,retry,retryCount}){
       // ...
       throw new Error()
    } 
}
  • 通过触发SideEffectTransitionError来阻止转换到错误状态
typescript
class MyStateMachine extends FlexStateMachine{
    async onInitialLeave({from,to,error,params,retry,retryCount}){
       // ...
       throw new SideEffectTransitionError()
    } 
}

当钩子函数触发SideEffectTransitionError代表着在该钩子函数中产生了不可消除的副作用,将使状态机转换到ERROR状态。ERROR状态是一个FINAL状态,代表状态机处于最终状态。后续只能通过reset方法来重置状态机。

错误处理

转换状态或者执行动作均可能会出错,出错有可能会产生了副作用,最主要的表现形式就是上下文数据被污染了,必须提供正确的错误处理才可以确保状态机的工作正常。当状态机发生错误时,常见的处理方式是:

  • 如果没有产生严重的副作用,则一般会进行重试恢复。
  • 如果产生难以消除的副作用,则应重置状态机。

当调用transition方法来转换状态时,会依次调用状态的<当前状态>/leave<目标状态>/enter<目标状态>/done钩子函数。其中leave/enter这两个拦截钩子可以通过触发错误来阻止状态的转换。那么问题来了,当拦截钩子触发错误时,应该怎么处理错误?

  • 当前状态是A,执行transition("B"),先执行A/leave离开A状态,然后执行B/enter时出错了,也就是说无法进入到B状态,此时状态机应该处于什么状态?

此时可以恢复到A状态,但是问题是我们在A/leave时已经离开了A状态,由于在A/leave时已经干了一个不可描述的事(产生的副作用了,已经污染了上下文),那么要简单地恢复到A状态就应需要同时恢复上下文相关的数据,否则就可能造成数据混乱,并且可能在下次A/leave时产生业务混乱。可见,如果恢复到A状态,我们需要同时恢复当前的上下文数据。因此,问题就取决于,我们是否可以恢复上下文数据。如果可以,那么就可以安全的恢复到A状态;如果不行,则应该将状态机置为错误状态,让应用对状态机进行重置。

  • 当前状态是A,执行transition("B"),先执行A/leave离开A状态时出错

取决于在A/leave函数里面产生了多少副作用,如果产生的副作用是可消除的,则当前状态应保持不变; 如果副作用不可消除,则应该转换到错误状态,然后通过重置状态机来重新恢复上下文以消除副作用。

因此,当拦截钩子触发错误时,错误处理方式就有两种:

  • 如果产生的副作用是可消除的,则触发错误或返回false时回退到原始状态
  • 如果产生的副作用是不可消除的,则触发SideEffectTransitionError转换到ERROR状态

状态机提供了相应的方法来处理这些副作用。

重试

所有的钩子函数均传入retryretryCount两个参数用来实现重试操作。当产生的副作用是可消除的时,可以由开发者来自行决定如何进行重试。

  • retry(interval) :重试函数,能指定重试间隔
  • retryCount :代表第几次重试
typescript
import {RetrySignal } from "FlexStateMachine"
class MyStateMachine extends FlexStateMachine{
    async onALeave({from,to,retry,retryCount}){
       try{
           // .....干活,产生副作用,污染了上下文....
       }catch(e){
           // 如果副作用是可消除的,则进行重试
           if(retryCount<3){					 // 第几次重试
             retry(1000)						   // 延迟1秒后重试
           }else{
              throw e								// 导致转换出错,将恢复原始状态
           }
       }        
    }  
}    
// 上述retryCount<3时重试,就导致会重试3次,包括第一次执行,总共执行4次

触发转换错误

当执行钩子函数失败时,取决于在哪一个阶段出错,其处理方式是不同的

A/leave出错

  • 确认不会产生副作用,则只要抛出错误,状态机将保持状态不变。
  • 如果确认会产生不可消除的副作用,则需要抛出SideEffectTransitionError,状态将转换到ERROR状态
typescript
import { SideEffectTransitionError,FlexStateMachine } from "FlexStateMachine"
class MyStateMachine extends FlexStateMachine{
    async onALeave({from,to,retryIndex}){
       try{
           // .....干活,是否产生副作用,污染了上下文???
       }catch(e){           
         // 产生错误时可以有两个选择:
         // 1、如果确认没有产生副作用,则可以直接抛出错误,状态将保持不变
         throw e                				
         // 2、如果确认会产生不可消除的副作用,则需要抛出SideEffectTransitionError,状态将转换到ERROR状态
         throw new SideEffectTransitionError()  
       }        
    }
    async onBEnter(){      
        throw new Error()    
    }
}

B/enter出错

如果在B/enter阶段产生错误,则需要在a/resume回调中处理消除副作用。如果a/resume也抛出错误,则会转换到错误状态。

typescript
import {RetrySignal } from "FlexStateMachine"
class MyStateMachine extends FlexStateMachine{
    async onALeave({from,to}){ }
    async onAResume({from,to}){
      // 当b/enter出错后,应该重新恢复到A状态,可以在此消除ALeave产生的的副作用。执行后会恢复到A状态
      ......
    }
    async onBEnter(){      
        throw new Error()    
    }
}

由于在A->B的转换过程中会先执行A/leave,在其中可能会产生副作用。因此在B/enter出错时(即无法进入B状态),触发A/resume事件,开发者可以在A/resume钩子中来消除副作用。如果A/resume钩子函数中能成功消副作用,则状态机将保持在A状态;如果A/resume钩子函数触发了错误,则代表着无法消除副作用,因此状态机将转换到ERROR状态。

转换到ERROR状态

如果开发者明确钩子函数产生的副作用是不可消除的,则可以通过throw new SideEffectTransitionError()来强制转换到ERROR状态。此时状态机会直接触发ERROR/done事件,但是不会触发<当前状态>/leaveERROR/enter事件。

typescript
class MyStateMachine extends FlexStateMachine{
    async onALeave({from,to}){
        throw new SideEffectTransitionError()
    }
    async onAResume({from,to}){
      throw new SideEffectTransitionError()
    }
    async onBEnter(){      
        throw new SideEffectTransitionError()
    }
}

转换事件

状态转换过程中的钩子事件订阅:

typescript
let fsm = new FlexStateMachine({ })
// 在状态机上订阅
fsm.on("<状态名称>/enter",callback)
fsm.on("<状态名称>/leave",callback)
fsm.on("<状态名称>/done",callback)
fsm.on("<状态名称>/resume",callback)
//在状态上订阅
fsm.states.[状态名称].on("enter",callback)
fsm.states.[状态名称].on("leave",callback)
fsm.states.[状态名称].on("done",callback)
fsm.states.[状态名称].on("resume",callback)

Released under the MIT License.