import type { ZWaveError } from "@zwave-js/core" ; import { assign , createMachine , Interpreter , MachineOptions , sendParent , StateMachine , } from "xstate" ; import { messageIsPing } from "../commandclass/NoOperationCC" ; import { SendDataMulticastBridgeRequest } from "../controller/SendDataBridgeMessages" ; import { SendDataMulticastRequest } from "../controller/SendDataMessages" ; import { isSendData , isTransmitReport , SendDataMessage , } from "../controller/SendDataShared" ; import type { Message } from "../message/Message" ; import type { CommandQueueEvent } from "./CommandQueueMachine" ; import { createMessageDroppedUnexpectedError , sendDataErrorToZWaveError , ServiceImplementations , } from "./StateMachineShared" ; import type { Transaction } from "./Transaction" ; export interface TransactionMachineContext { transaction : Transaction ; sendDataAttempts : number ; result ? : Message ; error ? : ZWaveError ; } export type TransactionMachineEvent = ( CommandQueueEvent & { type : "command_success" ; } ) | ( CommandQueueEvent & { type : "command_failure" ; } ) | ( CommandQueueEvent & { type : "command_error" ; } ) | { type : "NIF" ; nodeId : number ; } | { type : "resend" ; } | { type : "stop" ; } ; const guards = { mayRetry : ( ctx , evt ) => { const msg = ctx . transaction . parts . current ; if ( ! isSendData ( msg ) ) return false ; if ( msg instanceof SendDataMulticastRequest || msg instanceof SendDataMulticastBridgeRequest ) { if ( evt . reason === "callback NOK" ) { return false ; } } return msg . maxSendAttempts > ctx . sendDataAttempts ; } , currentMessageIsSendData : ( ctx ) => isSendData ( ctx . transaction . parts . current ) , currentTransactionIsPingForNode : ( ctx , evt ) => { const msg = ctx . transaction . parts . current ; return ( ! ! msg && messageIsPing ( msg ) && msg . getNodeId ( ) === ( evt as any ) . nodeId ) ; } , hasMessage : ( ctx ) => ! ! ctx . transaction . parts . current } ; const every = ( ... guards ) => ( { type : "every" , guards } ) ; export type TransactionMachine = StateMachine < TransactionMachineContext , any , TransactionMachineEvent > ; export type TransactionMachineInterpreter = Interpreter < TransactionMachineContext , any , TransactionMachineEvent > ; export function createTransactionMachine ( id , transaction , implementations ) { return createMachine < TransactionMachineContext , TransactionMachineEvent > ( { preserveActionOrder : true , id , initial : "init" , context : { transaction , sendDataAttempts : 0 } , on : { NIF : { cond : "currentTransactionIsPingForNode" , actions : "resolvePing" , target : "done" , internal : true } , resend : { target : "execute" , internal : true } , stop : "done" } , states : { init : { always : { actions : "startTransaction" , target : "nextMessage" } } , nextMessage : { invoke : { id : "nextMessage" , src : "nextMessage" , onDone : [ { cond : "hasMessage" , target : "attemptMessage" , actions : "resetSendDataAttempts" } , { target : "done" } , ] , onError : { target : "done" } } } , attemptMessage : { always : [ { cond : "currentMessageIsSendData" , actions : "incrementSendDataAttempts" , target : "execute" } , { target : "execute" } , ] } , execute : { entry : "sendToCommandQueue" , on : { command_success : [ { actions : "rememberCommandSuccess" , target : "nextMessage" } , ] , command_failure : [ { cond : every ( "currentMessageIsSendData" , "mayRetry" ) , target : "retryWait" } , { actions : "rememberCommandFailure" , target : "nextMessage" } , ] , command_error : [ { cond : every ( "currentMessageIsSendData" , "mayRetry" ) , target : "retryWait" } , { actions : "rememberCommandError" , target : "nextMessage" } , ] } } , retryWait : { invoke : { id : "notify" , src : "notifyRetry" } , after : { 500 : "attemptMessage" } } , done : { type : "final" , entry : sendParent ( { type : "transaction_done" , id } ) } } } , { actions : { startTransaction : ( ctx ) => { ctx . transaction . parts . start ( ) ; } , resetSendDataAttempts : assign ( { sendDataAttempts : ( _ ) => 0 } ) , incrementSendDataAttempts : assign ( { sendDataAttempts : ( ctx ) => ctx . sendDataAttempts + 1 } ) , sendToCommandQueue : sendParent ( ( ctx ) => ( { type : "forward" , to : "QUEUE" , from : id , payload : { type : "add" , transaction : ctx . transaction } } ) ) , resolvePing : ( ctx ) => { ctx . transaction . parts . self ! . throw ( undefined as any ) . catch ( ( ) => { } ) ; } , rememberCommandSuccess : assign ( { result : ( _ , evt ) => evt . result , error : ( _ ) => undefined } ) , rememberCommandFailure : assign ( ( ctx , evt ) => { if ( ( isSendData ( ctx . transaction . parts . current ) || isTransmitReport ( evt . result ) ) && evt . reason === "callback NOK" ) { return { ... ctx , result : evt . result , error : undefined } ; } else { return { ... ctx , result : undefined , error : sendDataErrorToZWaveError ( evt . reason , ctx . transaction , evt . result ) } ; } } ) , rememberCommandError : assign ( { result : ( _ ) => undefined , error : ( _ , evt ) => createMessageDroppedUnexpectedError ( evt . error ) } ) , unsetCommandResult : assign ( { result : ( _ ) => undefined , error : ( _ ) => undefined } ) } , services : { nextMessage : ( ctx ) => { if ( ctx . error ) { implementations . rejectTransaction ( ctx . transaction , ctx . error ) ; return Promise . resolve ( ) ; } else { return ( ctx . transaction . parts . self ?. next ( ctx . result as any ) ?? Promise . resolve ( ) ) ; } } , notifyRetry : ( ctx ) => { implementations . notifyRetry ?. ( "SendData" , undefined , ctx . transaction . message , ctx . sendDataAttempts , ( ctx . transaction . message as SendDataMessage ) . maxSendAttempts , 500 ) ; return Promise . resolve ( ) ; } } , guards : { ... guards , every : ( ctx , event , { cond } ) => { const keys = ( cond as any ) . guards as string [ ] ; return keys . every ( ( guardKey ) => guards [ guardKey ] ( ctx , event , undefined as any ) ) ; } } , delays : { } } ) ; }