KStateMachine / kstatemachine

KStateMachine is a Kotlin DSL library for creating state machines and statecharts.
https://kstatemachine.github.io/kstatemachine/
Boost Software License 1.0
339 stars 19 forks source link

Pass data in FinishedEvent from final DataState. #57

Closed nsk90 closed 1 year ago

nsk90 commented 1 year ago

When we have sub-states and they have a finalDataState, when it is reached, I want to be somehow able to extract the data from FinishedEvent and depending on this data move to some other state in parent.

Currently FinishedEvent is does not have a data field. In xstate.js IIRC all events have "payload" field. In KStateMachine this is explicit: Eventand DataEvent. Maybe FinishedEvent can be data event? Or would this be not the right solution... I'm not sure, wanted to describe the usecase, maybe you'll be able to think of a nice solution.

Here is the sample of what I have tried to do:

data class Ev(override val data: Int) : DataEvent<Int>

fun main() {
  val m = createStateMachine {
    val s2 = state("s2")
    val s3 = state("s3")

    initialState("s1") {
      val childFinal = finalDataState<Int>("child_final")

      initialState("child_first") {
        dataTransition<Ev, Int> {
          targetState = childFinal
        }
      }

      transitionOn<FinishedEvent> {
        targetState = {
          // val data = event.data <-- cannot be done, FinishedEvent doesn't have 'data'

          // Then for some reason I thought that maybe event.state will be `childFinal`
          // and I can extract data from there.
          // this compiles, but crashes at runtime, 
          // because event.state is "s1" actually (and this is correct)
          val data = (event.state as DataState<Int>).data
          if (data == 3) s2 else s3
        }
      }
    }
  }

  m.processEvent(Ev(3)) // expecting to go to s2
  // OR
  m.processEvent(Ev(5)) // expecting to go to s3
}

Originally posted by @dimsuz in https://github.com/nsk90/kstatemachine/issues/54#issuecomment-1271914942

nsk90 commented 1 year ago

workaround: https://github.com/nsk90/kstatemachine/issues/54#issuecomment-1272067505

nsk90 commented 1 year ago

done https://github.com/nsk90/kstatemachine/releases/tag/v0.17.0

dimsuz commented 1 year ago

This works great now, thank you!

dimsuz commented 1 year ago

Now I stumbled into the next thing I'm not sure how to resolve :) Perhaps you will have some ideas how to implement this or suggest a workaround.

The thing is that when I receive the FinishedEvent I want to select targetState and it can be either of FinalDataState or a simple State.

But currently I cannot do that because I handle FinishedEvent by using transition dsl builder and it doesn't allow me to set FinalDataState as a targetState, because it accepts only inheritors of State.

The code may illustrate a bit better:

  createStateMachine {
    lateinit var state2: State
    lateinit var finalDataState: FinalDataState<Int>

    state("state1") {
      initialState("state12") {

      }

      transitionOn<FinishedEvent> {
        targetState = {
          when(event.data as Result) {
            Result.Success -> state2 // <-- this is State
            Result.Error -> {
              // somehow switch to FinalDataState and pass Int = 42 as data...??? 
              finalDataState
            }
          }
        }
      }
    }

    state2 = state("state2")
    finalDataState = finalDataState<Int>("final1")
  }

What I tried:

nsk90 commented 1 year ago

Yeah, this is expected behaviour. I call it transition type-safety =) You should be able to bypass it by using transitionConditionally function to define transition. Please try it (there is no limitation to State subclasses)

But be careful. When you set DataState as a target of some transition, this transition currently must be triggered only by DataEvent not just simple Event. (or exception is thrown)

Generally speaking this limitation may be released as in recent library versions I have added defaultData and lastData to DataState, so now technically it may be entered with any event type without exception (if default value was supplied).

So in your case FinalEvent is not DataEvent and I suppose you may see this exception error("$event does not contain data required by $this"). If everything works like I expect, I think exception may be replaced with usage of lastData now, same like it is done for implicit DataState activation.