statelyai / xstate

Actor-based state management & orchestration for complex app logic.
https://stately.ai/docs
MIT License
26.87k stars 1.23k forks source link

Ability to set history of a state that is not the current state in a running machine #233

Closed rodinhart closed 5 years ago

rodinhart commented 5 years ago

Bug or feature request?

Feature request. See #197 and #185

Description:

I have a running machine, in say state C, and whenever I transition from C to D, which will happen many times, I'd like to populate the history of say nested states E, F and G, so that when the machine transition from D to E for instance this history is used.

(Feature) Potential implementation:

The method state.fromDefinition doesn't suit me because:

  1. I have existing viewstate that is not in the serialized state form.
  2. If my statechart changes shape, I don't want to have to transform my stored viewstate, and these will be long living (years)

I'm looking for a more direct way of populating history, that isn't tied to the internals of the machine, only to the statechart.

Link to reproduction or proof-of-concept:

This is how a currently solve it (pseudo code):

machine = new Machine(statechart)
state = machine.initial

state = machine.transition("to-B")
state = machine.transition("to-C")

state = machine.transition("to-D")
// some action occurs on transition from C to D
disableActions()
state = machine.transition("to-E2") // E2 comes from the viewstate
state = machine.transition("to-F3") // again, viewstate
state = machine.transition("to-D")
enableActions()

// viewstate is now "loaded"
state = machine.transition("to-E") // will end up in E2 in this case.

I'd rather write something like:

machine.setHistory(state, "{A: {B: {C: {D: {E: "E2"}}}}")
davidkpiano commented 5 years ago

machine.setHistory(state, "{A: {B: {C: {D: {E: "E2"}}}}")

This can never happen, because the machine must always be treated as immutable (i.e., no dynamically defined statecharts).

I'd like to see your specific use-case, because it sounds like your E, F, and G states are dependent on the D state. This indicates a hierarchical structure (e.g., D.E instead of E, etc.)

Let's see a real, non-contrived example and work with that instead. Perhaps there's a different (better?) way to achieve this use-case.

rodinhart commented 5 years ago

Sorry, that was confusing, I meant:

state = machine.setHistory(...)

I can't give the actual use-case, as it is way too long, but I'll try to boil down the essence:

{
  "key": "example",
  "initial": "files",
  "states": {
    "files": {
      "on": {
        "open-file": "editor.hist"
      }
    },
    "editor": {
      "type": "parallel",
      "states": {
        "text-wrap": {
          "initial": "off",
          "states": {
            "hist": {
              "history": true
            },
            "off": {
              "on": {
                "wrap-on": "on"
              }
            },
            "on": {
              "on": {
                "wrap-off": "off"
              }
            }
          }
        }
      },
      "on": {
        "close-file": "files"
      }
    }
  }
}

image

Imagine a text editor, where you open a file to edit, and in the editor you can set the text wrapping to on/off. The state "text-wrap" has history (not in diagram), and when I open a file it should restore the history of "text-wrap" as it was when the editor was closed, and then transition into "hist". This viewstate would be stored with the contents of the file. A user will edit several files in a single session (hence single live time of a state machine), and there are many (hierarchical) states before "files" and parallel to "editor".

Hope this makes sense?

davidkpiano commented 5 years ago

@rodinhart Just catching up, were you able to find a different way of doing this?

rodinhart commented 5 years ago

I employ a workaround: I ignore actions while:

Not ideal, but it works.

davidkpiano commented 5 years ago

As of version 4, you can use State.create() to rehydrate a serialized state:

import { State } from 'xstate';
import { interpret } from 'xstate/lib/interpreter';
import { myMachine } from '../path/to/myMachine';

const serializedStateWithHistory = JSON.stringify(currentState);

// later...

const initialState = State.create(JSON.parse(serializedStateWithHistory));

const service = interpret(myMachine)
  .start(initialState); // <- start at specified state

This will retain and restore history (see .historyValue). See https://xstate.js.org/docs/guides/states.html#persisting-state for more info.