import { getErrorMessage } from "@zwave-js/shared" ; import { SortedList } from "alcalzone-shared/sorted-list" ; import { ActionObject , ActorRefFrom , assign , AssignAction , Interpreter , Machine , spawn , StateMachine , } from "xstate" ; import { forwardTo , pure , raise , sendParent , stop } from "xstate/lib/actions" ; import { isSendData } from "../controller/SendDataShared" ; import type { Message } from "../message/Message" ; import { createSerialAPICommandMachine , SerialAPICommandDoneData , SerialAPICommandMachine , SerialAPICommandMachineParams , } from "./SerialAPICommandMachine" ; import { respondUnsolicited , ServiceImplementations , } from "./StateMachineShared" ; import type { Transaction } from "./Transaction" ; export interface CommandQueueStateSchema { states : { idle : { } ; execute : { } ; abortSendData : { } ; executeDone : { } ; } ; } export interface CommandQueueContext { queue : SortedList < Transaction > ; callbackIDs : WeakMap < Transaction , string > ; currentTransaction ? : Transaction ; abortActor ? : ActorRefFrom < SerialAPICommandMachine > ; } export type CommandQueueEvent = { type : "trigger" ; } | { type : "add" ; transaction : Transaction ; from : string ; } | { type : "ACK" ; } | { type : "CAN" ; } | { type : "NAK" ; } | { type : "message" ; message : Message ; } | { type : "remove" ; transaction : Transaction ; } | { type : "command_error" ; error : Error ; } | ( { type : "command_success" ; } & Omit < CommandQueueDoneData & { type : "success" ; } , "type" > ) | ( { type : "command_failure" ; } & Omit < CommandQueueDoneData & { type : "failure" ; } , "type" > ) ; export type CommandQueueDoneData = SerialAPICommandDoneData & { transaction : Transaction ; } ; export type CommandQueueMachine = StateMachine < CommandQueueContext , CommandQueueStateSchema , CommandQueueEvent > ; export type CommandQueueInterpreter = Interpreter < CommandQueueContext , CommandQueueStateSchema , CommandQueueEvent > ; const setCurrentTransaction = assign ( ( ctx ) => ( { ... ctx , currentTransaction : ctx . queue . shift ( ) ! } ) ) ; const deleteCurrentTransaction = assign ( ( ctx ) => { ctx . callbackIDs . delete ( ctx . currentTransaction ! ) ; return { ... ctx , currentTransaction : undefined } ; } ) ; const stopTransaction = sendParent ( ( ctx , evt ) => ( { type : "forward" , from : "QUEUE" , to : ctx . callbackIDs . get ( evt . transaction ) , payload : { type : "stop" } } ) ) ; const removeFromQueue = assign ( ( ctx , evt ) => { ctx . queue . remove ( evt . transaction ) ; ctx . callbackIDs . delete ( evt . transaction ) ; return ctx ; } ) ; const stopAbortMachine = pure ( ( ctx ) => { const ret = [ ] ; if ( ctx . abortActor ) { ret . push ( stop ( ctx . abortActor . id ) ) ; } ret . push ( assign ( { abortActor : ( _ ) => undefined } ) ) ; return ret ; } ) ; const notifyResult = sendParent ( ( ctx , evt ) => ( { type : "forward" , from : "QUEUE" , to : ctx . callbackIDs . get ( ctx . currentTransaction ! ) , payload : { ... evt . data , type : evt . data . type === "success" ? "command_success" : "command_failure" } } ) ) ; const notifyError = sendParent ( ( ctx , evt ) => ( { type : "forward" , from : "QUEUE" , to : ctx . callbackIDs . get ( ctx . currentTransaction ! ) , payload : { type : "command_error" , error : evt . data , transaction : ctx . currentTransaction } } ) ) ; export function createCommandQueueMachine ( implementations , params ) { const spawnAbortMachine = assign ( { abortActor : ( _ ) => spawn ( createSerialAPICommandMachine ( implementations . createSendDataAbort ( ) , implementations , params ) ) } ) ; return Machine < CommandQueueContext , CommandQueueStateSchema , CommandQueueEvent > ( { preserveActionOrder : true , id : "CommandQueue" , initial : "idle" , context : { queue : new SortedList ( ) , callbackIDs : new WeakMap ( ) } , on : { add : { actions : [ assign ( ( ctx , evt ) => { ctx . queue . add ( evt . transaction ) ; ctx . callbackIDs . set ( evt . transaction , evt . from ) ; return ctx ; } ) , raise ( "trigger" ) as any , ] } , remove : [ { cond : "isCurrentTransactionAndSendData" , actions : [ spawnAbortMachine , stopTransaction ] } , { cond : "isNotCurrentTransaction" , actions : [ stopTransaction , removeFromQueue ] } , ] , message : [ { cond : "isExecuting" , actions : forwardTo ( "execute" ) } , { actions : respondUnsolicited } , ] , ACK : [ { cond : "isAbortingInFlight" , actions : forwardTo ( ( ctx ) => ctx . abortActor ! ) } , { cond : "isAbortingWithTimeout" , actions : forwardTo ( "executeSendDataAbort" ) } , { cond : "isExecuting" , actions : forwardTo ( "execute" ) } , ] , CAN : [ { cond : "isAbortingInFlight" , actions : forwardTo ( ( ctx ) => ctx . abortActor ! ) } , { cond : "isAbortingWithTimeout" , actions : forwardTo ( "executeSendDataAbort" ) } , { cond : "isExecuting" , actions : forwardTo ( "execute" ) } , ] , NAK : [ { cond : "isAbortingInFlight" , actions : forwardTo ( ( ctx ) => ctx . abortActor ! ) } , { cond : "isAbortingWithTimeout" , actions : forwardTo ( "executeSendDataAbort" ) } , { cond : "isExecuting" , actions : forwardTo ( "execute" ) } , ] } , states : { idle : { entry : deleteCurrentTransaction , on : { trigger : "idle" } , always : { target : "execute" , actions : setCurrentTransaction , cond : "queueNotEmpty" } } , execute : { invoke : { id : "execute" , src : "executeSerialAPICommand" , onDone : [ { cond : "isAbortingInFlight" , target : "executeDone" } , { cond : "executeSuccessful" , actions : notifyResult , target : "executeDone" } , { cond : "isSendDataWithCallbackTimeout" , target : "abortSendData" , actions : notifyResult } , { target : "executeDone" , actions : notifyResult } , ] , onError : { target : "executeDone" , actions : notifyError } } } , abortSendData : { invoke : { id : "executeSendDataAbort" , src : "executeSendDataAbort" , onDone : "executeDone" } } , executeDone : { always : { target : "idle" , actions : [ deleteCurrentTransaction , stopAbortMachine , ] } } } } , { services : { executeSerialAPICommand : ( ctx ) => { try { return createSerialAPICommandMachine ( ctx . currentTransaction ! . parts . current ! , implementations , params ) ; } catch ( e ) { implementations . log ( `Unexpected error during SerialAPI command: ${ getErrorMessage ( e , true ) }` , "error" ) ; return Promise . reject ( e ) ; } } , executeSendDataAbort : ( _ ) => createSerialAPICommandMachine ( implementations . createSendDataAbort ( ) , implementations , params ) } , guards : { executeSuccessful : ( _ , evt ) => evt . data ?. type === "success" , queueNotEmpty : ( ctx ) => ctx . queue . length > 0 , isNotCurrentTransaction : ( ctx , evt ) => ctx . currentTransaction !== evt . transaction , isCurrentTransactionAndSendData : ( ctx , evt ) => ctx . currentTransaction === evt . transaction && isSendData ( evt . transaction . message ) , currentTransactionIsSendData : ( ctx ) => isSendData ( ctx . currentTransaction ?. parts . current ) , isSendDataWithCallbackTimeout : ( ctx , evt ) => { return ( isSendData ( ctx . currentTransaction ?. parts . current ) && evt . data ?. type === "failure" && evt . data ?. reason === "callback timeout" ) ; } , isExecuting : ( ctx , evt , meta ) => meta . state . matches ( "execute" ) , isAbortingWithTimeout : ( ctx , evt , meta ) => meta . state . matches ( "abortSendData" ) , isAbortingInFlight : ( ctx ) => ctx . abortActor != undefined } , delays : { } } ) ; }